Functional
Python admits substantial functional-style programming. First-class functions, lambdas, closures, comprehensions, generator expressions, the functools module, and the operator module together provide a rich functional vocabulary. The language is not purely functional and does not enforce immutability, but the patterns it supports cover most practical functional programming use cases. Modern Python uses functional idioms widely — map/filter for transformations, reduce for aggregation, lru_cache for memoisation, comprehensions throughout — though the conventional Python style favours explicit loops and comprehensions over heavy map/filter/reduce chains.
This page covers the functional facilities; the iterator-specific topics are in Iterators and generators, and the decorator surface is in Decorators.
First-class functions
Functions in Python are objects with attributes:
def square(n):
return n * n
square.__name__ # 'square'
square.__doc__ # docstring
square.__module__ # the module
# Functions can be assigned, passed, returned:
fn = square
print(fn(5)) # 25
# Stored in dicts:
operations = {"square": square, "double": lambda n: n * 2}
operations["square"](5) # 25
The mechanism admits substantial higher-order programming.
Lambdas
Lambda expressions are anonymous one-expression functions:
square = lambda n: n ** 2
add = lambda x, y: x + y
The body must be a single expression; statements are not admitted. Treated in Functions.
Higher-order functions
The standard library provides several:
# map: apply a function to every element
list(map(lambda x: x ** 2, [1, 2, 3, 4])) # [1, 4, 9, 16]
# filter: keep elements satisfying a predicate
list(filter(lambda x: x > 0, [-1, 2, -3, 4])) # [2, 4]
# zip: pair elements
list(zip([1, 2, 3], ['a', 'b', 'c'])) # [(1, 'a'), (2, 'b'), (3, 'c')]
# sorted: sort with key
sorted(items, key=lambda x: x.priority)
The map and filter return iterators, not lists; wrap with list(...) to materialise.
The conventional Python style often prefers comprehensions to map/filter:
# map:
[x ** 2 for x in xs] # idiomatic
list(map(lambda x: x ** 2, xs)) # equivalent but verbose
# filter:
[x for x in xs if x > 0] # idiomatic
list(filter(lambda x: x > 0, xs)) # equivalent
# map and filter:
[x ** 2 for x in xs if x > 0] # one comprehension
list(map(lambda x: x ** 2, filter(lambda x: x > 0, xs))) # nested
The comprehension form is conventionally clearer; map and filter are appropriate when the function is already named (map(int, strings)).
functools.reduce
reduce collapses an iterable to a single value:
from functools import reduce
reduce(lambda a, b: a + b, [1, 2, 3, 4]) # 10
reduce(lambda a, b: a + b, [1, 2, 3, 4], 100) # 110 (with initial)
reduce(lambda a, b: a * b, range(1, 6)) # 120 (5!)
For sums, sum() is preferable; for products, math.prod() (since 3.8). For arbitrary reductions, reduce is the conventional fallback.
The reduce was a built-in in Python 2; in Python 3 it lives in functools.
functools essentials
The functools module provides several functional combinators:
partial
Partial application:
from functools import partial
def multiply(a, b):
return a * b
double = partial(multiply, 2)
double(5) # 10
triple = partial(multiply, 3)
triple(5) # 15
# Equivalent with lambda:
double = lambda b: multiply(2, b)
partial produces a new callable with some arguments fixed. The mechanism is the conventional Python form for partial application; lambda is also admitted.
For keyword arguments:
import logging
debug = partial(logging.log, level=logging.DEBUG)
debug("a debug message")
reduce
Treated above.
lru_cache, cache
Memoisation:
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
Treated in Decorators.
cached_property
Per-instance cached property:
from functools import cached_property
class Document:
def __init__(self, content):
self.content = content
@cached_property
def word_count(self):
return len(self.content.split())
Computed once per instance on first access; cached thereafter.
singledispatch
Type-based dispatch:
from functools import singledispatch
@singledispatch
def show(value):
return repr(value)
@show.register
def _(value: int):
return f"int: {value}"
@show.register
def _(value: list):
return f"list of {len(value)}"
Treated in Functions.
wraps
Preserves metadata when wrapping:
from functools import wraps
def decorator(f):
@wraps(f)
def wrapped(*args, **kwargs):
return f(*args, **kwargs)
return wrapped
Treated in Decorators.
total_ordering
Generates comparison methods from __eq__ and one of __lt__, __le__, __gt__, __ge__:
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor, patch):
self.tuple = (major, minor, patch)
def __eq__(self, other):
return self.tuple == other.tuple
def __lt__(self, other):
return self.tuple < other.tuple
# total_ordering provides __le__, __gt__, __ge__ automatically
The decorator reduces boilerplate when implementing comparable classes.
reduce_*
A small set of additional combinators (reduce, accumulate from itertools).
operator module
The operator module exposes Python operators as functions:
import operator
operator.add(2, 3) # 5 (= 2 + 3)
operator.sub(5, 2) # 3
operator.mul(3, 4) # 12
operator.truediv(10, 4) # 2.5
operator.floordiv(10, 4) # 2
operator.mod(10, 3) # 1
operator.pow(2, 10) # 1024
operator.neg(5) # -5
operator.eq(1, 1) # True
operator.lt(1, 2) # True
operator.contains([1, 2, 3], 2) # True
operator.itemgetter(0)([10, 20]) # 10
operator.itemgetter("name")({"name": "alice"}) # "alice"
operator.attrgetter("x")(point) # point.x
operator.methodcaller("upper")("hello") # "HELLO"
The principal uses are key functions for sorted, min, max, groupby:
from operator import itemgetter, attrgetter
# Sort by index:
sorted(rows, key=itemgetter(2))
sorted(rows, key=itemgetter("priority"))
# Sort by attribute:
sorted(items, key=attrgetter("name"))
sorted(items, key=attrgetter("category", "name")) # multiple keys
# In place of lambda:
sorted(items, key=lambda x: x.name) # also works
sorted(items, key=attrgetter("name")) # equivalent; faster
The attrgetter and itemgetter are conventional shorthands; they admit slightly faster lookup than equivalent lambdas.
Comprehensions
Comprehensions are the conventional Python idiom for filter/map operations:
# List:
squares = [x ** 2 for x in range(10)]
positives = [x for x in numbers if x > 0]
pairs = [(x, y) for x in range(3) for y in range(3) if x != y]
# Set:
unique_words = {w.lower() for w in words}
# Dict:
char_counts = {c: text.count(c) for c in set(text)}
# Generator:
sum_of_squares = sum(x ** 2 for x in range(1000))
Treated in Loops and Iterators and generators.
Higher-order patterns
Map
# Comprehension:
squares = [x ** 2 for x in xs]
# map:
list(map(lambda x: x ** 2, xs))
# Comprehension with named function:
list(map(square, xs))
Filter
# Comprehension:
positives = [x for x in xs if x > 0]
# filter:
list(filter(lambda x: x > 0, xs))
Reduce
from functools import reduce
# Sum:
sum(xs)
# Product:
import math
math.prod(xs) # since 3.8
reduce(lambda a, b: a * b, xs, 1) # general
# Custom reduction:
words = "hello world from python".split()
longest = reduce(lambda a, b: a if len(a) > len(b) else b, words)
For the common reductions (sum, min, max, any, all, len), built-ins are preferable.
Pipeline
result = sum(
x ** 2
for x in numbers
if x > 0
)
The generator-expression pipeline admits filter-then-map-then-reduce in a single expression.
For more elaborate pipelines, libraries like toolz, funcy, or more-itertools provide additional combinators.
Closures
Functions defined inside other functions capture the enclosing scope:
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
double(5) # 10
triple(5) # 15
The closure captures factor from the enclosing scope. For mutable captures, nonlocal is required (treated in Modules).
Pure-function discipline
Python does not enforce purity, but the discipline of writing pure functions has substantial benefits:
- Pure functions are easier to test (no fixtures, no mocks).
- Pure functions are easier to reason about.
- Pure functions admit memoisation through
lru_cache. - Pure functions parallelise more easily.
The conventional discipline:
- Take all inputs as parameters; do not read global state.
- Return all outputs; do not mutate parameters.
- Avoid I/O in computation functions.
# Pure:
def calculate_total(orders, tax_rate):
return sum(o.amount for o in orders) * (1 + tax_rate)
# Less pure (depends on globals):
def calculate_total(orders):
return sum(o.amount for o in orders) * (1 + global_tax_rate)
The pure form is testable in isolation; the impure form requires setting global_tax_rate for tests.
Immutability
Python’s built-in immutables: int, float, bool, str, bytes, tuple, frozenset, range. Mutables: list, dict, set, bytearray.
For value-class-like immutables, dataclasses with frozen=True:
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
p = Point(3, 4)
p.x = 5 # FrozenInstanceError
For genuinely persistent (functional) data structures, third-party libraries (pyrsistent):
from pyrsistent import pvector, pmap
v = pvector([1, 2, 3])
v2 = v.append(4) # produces a new vector; v is unchanged
m = pmap({"a": 1, "b": 2})
m2 = m.set("c", 3)
The standard library does not include persistent collections; pyrsistent and similar libraries provide them. For most Python code, conventional mutation is acceptable.
Comprehensions vs map/filter — the conventional choice
The conventional Python style:
- Use comprehensions for transformations of a known iterable.
- Use
map/filterwhen the function is already a named function. - Use generator expressions when materialisation isn’t needed.
- Use the
functools.reducefor arbitrary reductions; for sums and products, use built-ins.
# Idiomatic:
[x ** 2 for x in xs]
# Reasonable:
list(map(int, string_numbers)) # named function
list(filter(None, items)) # filter falsy
# Less idiomatic (but valid):
list(map(lambda x: x ** 2, xs))
A note on the conventional discipline
Python is multi-paradigm; the functional facilities are tools, not a paradigm. Modern Python uses:
- Comprehensions for transformations.
- Generators for lazy sequences.
functools.lru_cachefor memoisation.- Pure functions for testability.
- Closures for stateful behaviour.
But not necessarily:
- Heavy
map/filterchains (comprehensions are clearer). reducefor everything (sum/min/max/all/any are built-ins).- Persistent data structures (mutation is conventional in Python).
- Currying or function composition as primary idioms.
The Pythonic functional style is light — apply functional tools where they help, fall back to imperative style elsewhere. The combination admits substantial expressive power without forcing one paradigm on every program.