Polyglot
Languages Python functional
Python § functional

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/filter when the function is already a named function.
  • Use generator expressions when materialisation isn’t needed.
  • Use the functools.reduce for 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_cache for memoisation.
  • Pure functions for testability.
  • Closures for stateful behaviour.

But not necessarily:

  • Heavy map/filter chains (comprehensions are clearer).
  • reduce for 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.