Functional Programming Techniques
Reviewed & published by Brayan K
Master functional programming patterns, pure functions, composition, and advanced FP architectures
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What Functional Programming Actually Means
Functional programming is based on core principles:
| Principle | What It Means | Real-World Analogy |
|---|---|---|
| Pure Functions | Same input → same output, no side effects | A calculator: 2+2 always equals 4 |
| Immutability | Data never changes; create new copies | Editing a photo creates a new file |
| First-Class Functions | Functions can be passed around like data | Giving someone a recipe card to follow |
| Higher-Order Functions | Functions that work with other functions | A chef who teaches other chefs techniques |
| Declarative Style | Describe what, not how | "Make me a sandwich" vs step-by-step instructions |
- Functions are first-class citizens — can be stored, passed, returned
- No mutating state — create new data instead of modifying
- Pure functions — no side effects, predictable output
- Immutability — treat data as unchangeable
- Declarative style — describe what you want, not how
Pure Functions
A pure function has no side effects, doesn't modify external state, and always produces the same output for the same input.
# Pure function - predictable, no side effects
def pure_add(x, y):
return x + y
print(pure_add(2, 3)) # Always returns 5
print(pure_add(2, 3)) # Still returns 5
# Impure function - has side effects
count = 0
def impure_increment():
global count
count += 1
return count
print(impure_increment()) # 1
print(impure_increment()) # 2 (different result!)
# Pure functions are:
# ✓ easier to test
# ✓ easier to debug
# ✓ safer to parallelize
# ✓ more reliable
# ✅ Expected output:
# 5
# 5
# 1
# 2Immutability in Python
FP encourages transforming data instead of mutating it. Use immutable types and return new values.
# Immutable types: int, float, str, tuple, frozenset
# Mutable types: list, dict, set
# FP style - transform, don't mutate
nums = [1, 2, 3, 4, 5]
# Good FP style - create new list
doubled = [x * 2 for x in nums]
print(f"Original: {nums}")
print(f"Doubled: {doubled}")
# Avoid mutation (when following FP)
# nums.append(6) # modifies original
# Using tuples for immutability
point = (3, 4)
# point[0] = 5 # Error! Tuples are immutable
# Frozen sets
fs = frozenset([1, 2, 3])
# fs.add(4) # Error! Frozen sets are immutable
print("\nFP encourages: every transformation returns new value")
# ✅ Expected output:
# Original: [1, 2, 3, 4, 5]
# Doubled: [2, 4, 6, 8, 10]
#
# FP encourages: every transformation returns new valueHigher-Order Functions
Functions that take functions as arguments or return functions. Core to functional programming.
# Higher-order function - takes function as argument
def apply_twice(func, value):
return func(func(value))
def add_one(x):
return x + 1
def double(x):
return x * 2
result1 = apply_twice(add_one, 5)
print(f"Apply add_one twice to 5: {result1}") # 7
result2 = apply_twice(double, 3)
print(f"Apply double twice to 3: {result2}") # 12
# Higher-order function - returns a function
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
times_3 = make_multiplier(3)
times_5 = make_multiplier(5)
print(f"\n3 × 4 = {times_3(4)}")
print(f"5 × 4 = {times_5(4)}")
# Used in: callbacks, decorators, event systems, ML pipelines
# ✅ Expected output:
# Apply add_one twice to 5: 7
# Apply double twice to 3: 12
#
# 3 × 4 = 12
# 5 × 4 = 20map(), filter(), reduce() — Core FP Tools
The fundamental functional transformations for working with sequences.
from functools import reduce
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# map() - transform each element
doubled = list(map(lambda x: x * 2, nums))
print(f"Doubled: {doubled}")
# filter() - keep only matching items
evens = list(filter(lambda x: x % 2 == 0, nums))
print(f"Evens: {evens}")
# reduce() - combine into single value
total = reduce(lambda a, b: a + b, nums)
print(f"Sum: {total}")
product = reduce(lambda a, b: a * b, nums)
print(f"Product: {product}")
# Chaining operations
result = list(map(
lambda x: x ** 2,
filter(lambda x: x % 2 == 0, nums)
))
print(f"\nSquares of evens: {result}")
# List comprehension equivalent (often more Pythonic)
result2 = [x ** 2 for x in nums if x % 2 == 0]
print(f"Same with comprehension: {result2}")
# ✅ Expected output:
# Doubled: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
# Evens: [2, 4, 6, 8, 10]
# Sum: 55
# Product: 3628800
#
# Squares of evens: [4, 16, 36, 64, 100]
# Same with comprehension: [4, 16, 36, 64, 100]Worked Example: One Basket, One Pipeline
Here are the three tools doing a job you would actually be asked to do: turn a shopping basket into line totals, pick out the big lines, and add everything up — without changing the original data. Every comment says what the line produces.
from functools import reduce
# Each row is (item, unit price, quantity).
basket = [("apple", 0.50, 4), ("bread", 2.20, 1), ("cheese", 4.75, 2), ("milk", 1.15, 3)]
# map() applies ONE function to every element and hands back a lazy iterator.
# "Lazy" means no work has happened yet - it is a promise, not a list.
line_totals = map(lambda row: (row[0], round(row[1] * row[2], 2)), basket)
# list() forces the promise to be kept, and now you have real data.
line_totals = list(line_totals)
print("line totals :", line_totals)
# filter() keeps only the elements where the function returns True.
expensive = list(filter(lambda pair: pair[1] >= 3, line_totals))
print("3 or more :", expensive)
# reduce() folds a whole list into ONE value, carrying a running result.
# The 0 at the end is the starting value - and the answer for an empty list.
total = reduce(lambda running, pair: running + pair[1], line_totals, 0)
print("basket total:", round(total, 2))
# The gotcha that catches everyone: an iterator is used up after ONE pass.
once = map(str.upper, ["a", "b"])
print("first pass :", list(once)) # ['A', 'B']
print("second pass :", list(once)) # [] - exhausted, not broken
# The same transformation as a comprehension. In Python this is usually the
# more readable choice; reach for map/filter when you already HAVE a function.
print("comprehension:", [(name, round(price * qty, 2)) for name, price, qty in basket])
# ✅ Expected output:
# line totals : [('apple', 2.0), ('bread', 2.2), ('cheese', 9.5), ('milk', 3.45)]
# 3 or more : [('cheese', 9.5), ('milk', 3.45)]
# basket total: 17.15
# first pass : ['A', 'B']
# second pass : []
# comprehension: [('apple', 2.0), ('bread', 2.2), ('cheese', 9.5), ('milk', 3.45)]Lambda Functions
Anonymous functions perfect for functional pipelines and quick transformations.
# Basic lambda syntax
square = lambda x: x * x
print(f"Square of 5: {square(5)}")
# Lambdas with multiple arguments
add = lambda x, y: x + y
print(f"3 + 4 = {add(3, 4)}")
# Lambdas in sorting
users = [
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25},
{"name": "Charlie", "age": 35}
]
sorted_by_age = sorted(users, key=lambda u: u["age"])
print("\nSorted by age:")
for user in sorted_by_age:
print(f" {user['name']}: {user['age']}")
# Lambdas in functional chains
nums = [1, 2, 3, 4, 5]
result = list(
map(lambda x: x * 2,
filter(lambda x: x > 2, nums)
)
)
print(f"\nProcessed: {result}")
# Used for: transformations, sorting keys, callbacks, inline rules
# ✅ Expected output:
# Square of 5: 25
# 3 + 4 = 7
#
# Sorted by age:
# Bob: 25
# Alice: 30
# Charlie: 35
#
# Processed: [6, 8, 10]🎯 Your Turn: Write the Four Lambdas
The plumbing is already in place. All that is missing is the small function inside each call — one for map, one for filter, one for reduce, and one to act as a sort key. Fill in each ___ and match the expected output.
from functools import reduce
# 🎯 YOUR TURN — replace each ___ with the body of the lambda.
nums = [3, 8, 1, 12, 7, 4]
tripled = list(map(lambda n: ___, nums)) # 👉 triple every number
print("tripled:", tripled)
big = list(filter(lambda n: ___, nums)) # 👉 keep only numbers greater than 5
print("big :", big)
# reduce runs left to right: 'a' is the result so far, 'b' is the next number.
largest = reduce(lambda a, b: ___, nums) # 👉 give back whichever is bigger
print("largest:", largest)
words = ["banana", "fig", "cherry", "kiwi"]
print("by length:", sorted(words, key=lambda w: ___)) # 👉 sort by word LENGTH
# ✅ Expected output:
# tripled: [9, 24, 3, 36, 21, 12]
# big : [8, 12, 7]
# largest: 12
# by length: ['fig', 'kiwi', 'banana', 'cherry']Function Composition
Building complex operations by chaining simple pure functions.
# Composition utility
def compose(*funcs):
def wrapper(x):
for f in reversed(funcs):
x = f(x)
return x
return wrapper
# Simple functions
double = lambda x: x * 2
increment = lambda x: x + 1
square = lambda x: x * x
# Compose them
f1 = compose(increment, double)
print(f"Double then increment 5: {f1(5)}") # 11
f2 = compose(square, increment, double)
print(f"Double, increment, square 3: {f2(3)}") # 49
# String processing pipeline
clean = compose(
str.strip,
str.lower,
lambda s: s.replace("!", "")
)
text = " HELLO WORLD! "
print(f"\nOriginal: '{text}'")
print(f"Cleaned: '{clean(text)}'")
# Used in: ML pipelines, data validation, text processing
# ✅ Expected output:
# Double then increment 5: 11
# Double, increment, square 3: 49
#
# Original: ' HELLO WORLD! '
# Cleaned: 'hello world'functools.partial — Pre-Configuring Functions
Create specialized versions of functions by "freezing" some arguments.
from functools import partial
import operator
# Basic partial application
def power(base, exp):
return base ** exp
square = partial(power, exp=2)
cube = partial(power, exp=3)
print(f"5² = {square(5)}")
print(f"5³ = {cube(5)}")
# With operators
times_10 = partial(operator.mul, 10)
result = list(map(times_10, [1, 2, 3, 4, 5]))
print(f"\nMultiply by 10: {result}")
# Practical example: logging with context
def log_message(level, message, context=""):
print(f"[{level}] {message} {context}")
log_error = partial(log_message, "ERROR")
log_info = partial(log_message, "INFO")
log_error("Database connection failed")
log_info("User logged in", context="user_id=123")
# Used in: ML preprocessing, API wrappers, event systems
# ✅ Expected output:
# 5² = 25
# 5³ = 125
#
# Multiply by 10: [10, 20, 30, 40, 50]
# [ERROR] Database connection failed
# [INFO] User logged in user_id=123Generator-Based Functional Programming
Generators provide lazy evaluation and memory-efficient functional pipelines.
# Generator pipeline for processing data
def numbers(n):
"""Generate numbers"""
for i in range(n):
yield i
def only_evens(nums):
"""Filter to only evens"""
for n in nums:
if n % 2 == 0:
yield n
def squared(nums):
"""Square each number"""
for n in nums:
yield n ** 2
# Chain generators (lazy evaluation)
pipeline = squared(only_evens(numbers(10)))
print("Processing numbers 0-9:")
for result in pipeline:
print(f" {result}")
# File processing pipeline
def read_lines(text):
"""Simulate file reading"""
lines = text.split("\n")
for line in lines:
yield line.strip()
def filter_errors(lines):
"""Keep only error lines"""
for line in lines:
if "ERROR" in line:
yield line
# Example log data
log_data = """INFO: System started
ERROR: Connection timeout
INFO: User logged in
ERROR: File not found
INFO: Request processed"""
print("\nError lines:")
for line in filter_errors(read_lines(log_data)):
print(f" {line}")
# ✅ Expected output:
# Processing numbers 0-9:
# 0
# 4
# 16
# 36
# 64
#
# Error lines:
# ERROR: Connection timeout
# ERROR: File not foundAdvanced: Currying
Transform multi-argument functions into chains of single-argument functions.
# Manual currying
def curry_add(a):
def add_b(b):
def add_c(c):
return a + b + c
return add_c
return add_b
result = curry_add(1)(2)(3)
print(f"Curried add: {result}")
# Generic curry decorator
def curry(func):
"""Transform f(a, b, c) into f(a)(b)(c)"""
def curried(a):
return lambda b: lambda c: func(a, b, c)
return curried
@curry
def multiply(a, b, c):
return a * b * c
result = multiply(2)(3)(4)
print(f"Curried multiply: {result}")
# Partial application of curried function
times_2 = multiply(2)
times_2_3 = times_2(3)
print(f"2 × 3 × 5 = {times_2_3(5)}")
# Practical use: configuration
def make_greeting(greeting):
def with_name(name):
def with_punctuation(punct):
return f"{greeting}, {name}{punct}"
return with_punctuation
return with_name
hello = make_greeting("Hello")
hello_alice = hello("Alice")
print(f"\n{hello_alice('!')}")
print(f"{hello_alice('.')}")
# ✅ Expected output:
# Curried add: 6
# Curried multiply: 24
# 2 × 3 × 5 = 30
#
# Hello, Alice!
# Hello, Alice.Decorators — Functional Power Tools
Decorators are higher-order functions that transform and enhance functions.
from functools import wraps
import time
# Simple logging decorator
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f" → {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
print("Example 1: Logging")
add(3, 5)
add(10, 20)
# Timing decorator
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_function():
time.sleep(0.1)
return "Done"
print("\nExample 2: Timing")
slow_function()
# Decorators stack
@log_calls
@timer
def compute(x):
time.sleep(0.05)
return x ** 2
print("\nExample 3: Stacked decorators")
compute(5)Functional Error Handling
Handle errors with values instead of exceptions, enabling safer composition.
# Option/Maybe pattern
class Maybe:
def __init__(self, value):
self.value = value
def bind(self, func):
if self.value is None:
return Maybe(None)
try:
return Maybe(func(self.value))
except:
return Maybe(None)
def get_or(self, default):
return self.value if self.value is not None else default
# Safe operations
def safe_divide(x):
return 100 / x if x != 0 else None
def double(x):
return x * 2
# Chain safely
result1 = Maybe(10).bind(safe_divide).bind(double)
print(f"Maybe(10) → divide → double: {result1.get_or('Error')}")
result2 = Maybe(0).bind(safe_divide).bind(double)
print(f"Maybe(0) → divide → double: {result2.get_or('Error')}")
# Result type (Success/Failure)
class Result:
def __init__(self, ok=None, err=None):
self.ok = ok
self.err = err
def bind(self, func):
if self.err:
return self
try:
return Result(ok=func(self.ok))
except Exception as e:
return Result(err=str(e))
# Safe parsing pipeline
def parse_int(s):
return int(s)
result = Result(ok="123").bind(parse_int).bind(lambda x: x * 2)
print(f"\nParse '123': {result.ok if result.ok else result.err}")
result = Result(ok="abc").bind(parse_int).bind(lambda x: x * 2)
print(f"Parse 'abc': {result.ok if result.ok else result.err}")
# ✅ Expected output:
# Maybe(10) → divide → double: 20.0
# Maybe(0) → divide → double: Error
#
# Parse '123': 246
# Parse 'abc': invalid literal for int() with base 10: 'abc'Immutable Data Structures
Use frozen dataclasses and immutable collections for safer functional code.
from dataclasses import dataclass
from typing import FrozenSet
# Frozen dataclass (immutable)
@dataclass(frozen=True)
class Point:
x: int
y: int
def move(self, dx, dy):
"""Returns new Point instead of modifying"""
return Point(self.x + dx, self.y + dy)
p1 = Point(0, 0)
p2 = p1.move(3, 4)
print(f"Original: {p1}")
print(f"Moved: {p2}")
# p1.x = 10 # Error! Frozen dataclass is immutable
# Frozen dataclass with methods
@dataclass(frozen=True)
class User:
id: int
name: str
email: str
def with_email(self, new_email):
"""Create new User with updated email"""
return User(self.id, self.name, new_email)
user = User(1, "Alice", "[email protected]")
updated = user.with_email("[email protected]")
print(f"\nOriginal user: {user.email}")
print(f"Updated user: {updated.email}")
# Frozen sets (immutable sets)
tags: FrozenSet[str] = frozenset(["python", "fp", "coding"])
# tags.add("new") # Error! Can't modify frozen set
print(f"\nTags: {tags}")
print("Frozen sets are hashable and thread-safe!")Real-World Functional Pipeline
A complete functional architecture for data processing.
from functools import reduce
# Pure transformation functions
def parse_line(line):
"""Parse CSV-like line"""
parts = line.strip().split(",")
return {"name": parts[0], "value": int(parts[1])}
def is_valid(record):
"""Check if record is valid"""
return record["value"] > 0
def extract_value(record):
"""Extract just the value"""
return record["value"]
def sum_values(acc, value):
"""Accumulator for reduce"""
return acc + value
# Sample data
data = """Alice,100
Bob,-50
Charlie,200
David,0
Eve,150"""
# Functional pipeline
lines = data.split("\n")
# Method 1: Chained operations
result = reduce(
sum_values,
map(
extract_value,
filter(
is_valid,
map(parse_line, lines)
)
),
0
)
print(f"Sum of valid values: {result}")
# Method 2: Generator pipeline (more readable)
def pipeline(lines):
parsed = (parse_line(line) for line in lines)
valid = (record for record in parsed if is_valid(record))
values = (extract_value(record) for record in valid)
return reduce(sum_values, values, 0)
result2 = pipeline(data.split("\n"))
print(f"Pipeline result: {result2}")
# Benefits:
# ✓ No side effects
# ✓ Fully testable
# ✓ Composable
# ✓ Memory efficient
# ✓ Easy to parallelize
# ✅ Expected output:
# Sum of valid values: 450
# Pipeline result: 450🎯 Mini-Challenge: A Revenue Report, Functionally
Build a small sales report as a pipeline: filter, then transform, then sort, then fold to a single number. The one hard rule is that orders must be untouched when you are finished — no .append(), no .sort(), no assigning into a dictionary.
# 🎯 MINI-CHALLENGE: a revenue report, functionally
#
# Start with this data (copy it in as-is):
#
# orders = [
# {"item": "keyboard", "price": 45.0, "qty": 2, "paid": True},
# {"item": "mouse", "price": 18.5, "qty": 1, "paid": False},
# {"item": "monitor", "price": 199.0, "qty": 1, "paid": True},
# {"item": "cable", "price": 6.25, "qty": 4, "paid": True},
# ]
#
# 1. filter out anything where "paid" is False
# 2. map each remaining order to a tuple: (item, price * qty rounded to 2 dp)
# 3. sort those tuples by value, LARGEST first (sorted(..., reverse=True))
# 4. print each tuple on its own line
# 5. reduce the values into one total and print revenue: <total>
#
# Rule: orders must be exactly as it started when you are done. Every step
# makes a new value rather than editing the old one.
#
# ✅ Expected output:
# ('monitor', 199.0)
# ('keyboard', 90.0)
# ('cable', 25.0)
# revenue: 314.0
# your code hereSummary
You've learned comprehensive functional programming in Python:
- Pure functions and immutability principles
- Higher-order functions and function composition
- map(), filter(), reduce() and lambda functions
- functools utilities (partial, lru_cache, reduce)
- Generator-based lazy evaluation
- Advanced patterns: currying, decorators, monads
- Functional error handling with Maybe/Result
- Immutable data structures
- Real-world functional pipelines
Functional programming helps you build cleaner, more predictable, and easier-to-maintain code. These patterns are essential for data engineering, ML pipelines, ETL systems, and modern backend architectures.
📋 Quick Reference — Functional Programming
| Tool / Syntax | What it does |
|---|---|
| map(fn, iterable) | Apply fn to every element |
| filter(fn, iterable) | Keep elements where fn returns True |
| functools.reduce(fn, iterable) | Accumulate into a single value |
| lambda x: x * 2 | Anonymous inline function |
| functools.partial(fn, arg) | Pre-fill some function arguments |
🎉 Great work! You've completed this lesson.
You can now write purely functional Python using map, filter, reduce, and composition — essential for data pipelines and ML workflows.
Practice quiz
What defines a pure function?
- It prints output
- It uses global state
- Same input always gives the same output with no side effects
- It modifies its arguments
Answer: Same input always gives the same output with no side effects. A pure function always returns the same output for the same input and has no side effects, making it predictable and testable.
Which of these Python types is IMMUTABLE?
- tuple
- list
- dict
- set
Answer: tuple. Tuples (like int, float, str, frozenset) are immutable; lists, dicts, and sets are mutable.
What does map(lambda x: x * 2, [1, 2, 3]) produce when wrapped in list()?
- [1, 2, 3]
- [1, 4, 9]
- 6
- [2, 4, 6]
Answer: [2, 4, 6]. map applies the function to each element, doubling them to give [2, 4, 6].
What does filter(lambda x: x % 2 == 0, nums) keep?
- Odd numbers
- Even numbers (where the function returns True)
- All numbers
- Nothing
Answer: Even numbers (where the function returns True). filter keeps only elements for which the function returns True; here that means the even numbers.
What does reduce(lambda a, b: a + b, [1, 2, 3, 4, 5]) return?
- 15
- 120
- [1,2,3,4,5]
- 5
Answer: 15. reduce combines the list into a single value by repeated addition: 1+2+3+4+5 = 15.
What is a higher-order function?
- A function with many lines
- A recursive function
- A function that takes or returns other functions
- A function defined at module top level
Answer: A function that takes or returns other functions. A higher-order function takes functions as arguments and/or returns functions — core to functional programming.
What does a lambda expression create?
- A class
- An anonymous (unnamed) inline function
- A generator
- A decorator
Answer: An anonymous (unnamed) inline function. lambda creates a small anonymous function, handy for sorting keys, callbacks, and inline transformations.
What advantage do generators give in functional pipelines?
- They sort data
- They run in parallel automatically
- They validate types
- Lazy evaluation — memory-efficient, one item at a time
Answer: Lazy evaluation — memory-efficient, one item at a time. Generators yield items lazily, so a chained generator pipeline processes data one element at a time without building large lists in memory.
What does functools.partial do?
- Splits a function in half
- Creates a specialized function by pre-filling (freezing) some arguments
- Runs a function partially
- Caches results
Answer: Creates a specialized function by pre-filling (freezing) some arguments. partial pre-fills some arguments to produce a new, specialized callable, e.g. square = partial(power, exp=2).
What is currying?
- Caching function results
- Running functions concurrently
- Transforming f(a, b, c) into a chain f(a)(b)(c) of single-argument calls
- Adding type hints
Answer: Transforming f(a, b, c) into a chain f(a)(b)(c) of single-argument calls. Currying turns a multi-argument function into a sequence of single-argument functions, called like multiply(2)(3)(4).
Continue this course
- Previous: Advanced Collections, itertools & functools
- Next: Metaprogramming & Introspection with inspect — Dynamically inspect, modify, and create classes and functions at runtime
- Quick reference: Python cheat sheet