🐍 Python Course · Stage 3 · Lesson 40 of 89 · functools — Function Tools
Course Overview →

🔵 Read Beginner and Intermediate tabs. The Expert tab has the internals — worth reading once you finish Stage 3.

← unittest — Testing csv — CSV Files →
thecodex.expert · The Codex Family of Knowledge
Python

functools — Higher-Order Functions

The functools module provides tools for working with functions: caching with lru_cache and cache, partial application with partial, reducing with reduce, and decorator-writing helpers like wraps.

Python 3.13 docs.python.org functools Last verified:
Canonical Definition

functools is a standard library module for higher-order functions — functions that act on or return other functions. Its most-used members are lru_cache/cache (memoisation decorators), partial (partial application), reduce (fold a sequence to a single value), and wraps (preserve metadata when writing decorators).

🟩 Beginner

Tools for working with functions

What you'll learn: The functools toolbox, and especially @lru_cache/@cache — decorators that make slow functions fast by remembering their results (memoisation).

How to read this tab: Write the naive recursive fibonacci, time fib(35), then add @cache and time it again. The speedup — from seconds to instant — is the most memorable way to understand memoisation.

⏱ 30 min📄 2 sections🔶 Prerequisite: Decorators
The idea

functools is a toolbox for working with functions themselves. The two tools you'll reach for most: @lru_cache to make slow functions fast by remembering results, and partial to create a new function with some arguments pre-filled.

💡

functools is the standard library's function toolbox. The high-value tools: @lru_cache/@cache (memoisation — the biggest performance win for the least effort), partial (preset arguments), @wraps (covered on the Decorators page — mandatory when writing decorators). Learn lru_cache first; it solves real performance problems with one line.

What functools provides

functools is part of the standard library — no installation needed. It contains higher-order functions: tools that take functions as arguments or return new functions.

ToolWhat it does
@lru_cacheCache a function's results (memoisation)
@cacheSimpler unbounded cache (Python 3.9+)
partial()Create a new function with some arguments fixed
reduce()Combine a sequence into a single value
@wrapsPreserve metadata when writing decorators
@singledispatchFunction overloading based on argument type
cmp_to_key()Convert old-style comparison functions to keys

@lru_cache is the single highest-impact tool in functools. Adding one decorator line can turn an exponential-time recursive function into a linear-time one. The rule: apply it to pure functions (same input always gives same output) that are called repeatedly with the same arguments. Never apply it to functions with side effects or those whose results change over time.

Caching with lru_cache and cache

The single most impactful functools tool: @lru_cache remembers the results of function calls. When the function is called again with the same arguments, the cached result is returned instantly instead of recomputing.

Pythonfunctools_cache.py
from functools import lru_cache, cache

# Without caching — fibonacci is exponentially slow
def fib_slow(n):
    if n < 2: return n
    return fib_slow(n-1) + fib_slow(n-2)
# fib_slow(35) makes ~30 million calls — takes seconds

# With caching — each value computed once
@lru_cache(maxsize=None)   # None = unbounded cache
def fib_fast(n):
    if n < 2: return n
    return fib_fast(n-1) + fib_fast(n-2)
# fib_fast(35) makes 36 calls — instant

print(fib_fast(100))   # 354224848179261915075 — instant

# @cache (Python 3.9+) is shorthand for @lru_cache(maxsize=None)
@cache
def expensive_lookup(key):
    print(f"Computing for {key}...")
    return key ** 2

print(expensive_lookup(5))   # Computing for 5... → 25
print(expensive_lookup(5))   # 25  (no "Computing" — cached)

# Inspect cache performance
print(fib_fast.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=None, currsize=101)

# Clear the cache if needed
fib_fast.cache_clear()
Cached functions need hashable arguments

@lru_cache stores results in a dict keyed by the arguments — so all arguments must be hashable. You can cache f(1, "x") but not f([1,2,3]) because lists aren't hashable. Pass tuples instead of lists for cacheable functions.

✅ Beginner tab complete

  • I can apply @lru_cache or @cache to memoise an expensive function
  • I know cached functions need hashable arguments (tuples, not lists)
  • I can inspect cache performance with func.cache_info()
  • I know @cache (3.9+) is shorthand for @lru_cache(maxsize=None)

Continue to csv →

🔵 Intermediate

partial, reduce, and singledispatch

What you'll learn: partial for creating specialised functions with preset arguments. reduce for folding sequences. singledispatch for type-based function overloading.

How to read this tab: Write a log function, then create info = partial(log, "INFO") and error = partial(log, "ERROR"). This pattern — preset configuration via partial — appears throughout real codebases.

⏱ 30 min📄 2 sections🔶 Prerequisite: Beginner tab
💡

partial creates a new function with some arguments locked in. It's cleaner than a lambda for this purpose: partial(log, "INFO") is clearer than lambda msg: log("INFO", msg). partial also plays well with introspection tools, whereas lambdas are anonymous. Use partial when you need to preset arguments; use lambda for arbitrary short inline functions.

partial application and reduce

partial creates a new function with some arguments already filled in. reduce combines all elements of a sequence into a single value by repeatedly applying a function.

Pythonfunctools_partial.py
from functools import partial, reduce

# partial — fix some arguments to create a specialised function
def power(base, exponent):
    return base ** exponent

square = partial(power, exponent=2)   # exponent is now fixed at 2
cube   = partial(power, exponent=3)

print(square(5))   # 25
print(cube(3))     # 27

# Real-world: partial for callbacks with preset config
def log(level, message):
    print(f"[{level}] {message}")

info  = partial(log, "INFO")
error = partial(log, "ERROR")
info("Server started")    # [INFO] Server started
error("Connection lost")  # [ERROR] Connection lost

# reduce — fold a sequence into one value
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda acc, x: acc + x, numbers)   # ((((1+2)+3)+4)+5)
print(total)   # 15

product = reduce(lambda acc, x: acc * x, numbers)  # 5! = 120
print(product)  # 120

# reduce with an initial value
total = reduce(lambda acc, x: acc + x, numbers, 100)  # starts at 100
print(total)   # 115

# Note: for sum/product, prefer the built-ins — they're clearer
print(sum(numbers))          # 15 — clearer than reduce
import math
print(math.prod(numbers))    # 120 — clearer than reduce
📎

@singledispatch brings type-based overloading to Python. Other languages let you define multiple functions with the same name but different parameter types. Python doesn't — but @singledispatch simulates it for the first argument's type. Register a separate implementation per type; Python picks the right one at call time based on the argument's type.

singledispatch — function overloading by type

@singledispatch lets you write a function that behaves differently based on the type of its first argument — Python's version of function overloading.

Pythonfunctools_dispatch.py
from functools import singledispatch

@singledispatch
def describe(value):
    """Default implementation."""
    return f"Unknown type: {value}"

@describe.register
def _(value: int):
    return f"Integer: {value} (binary: {value:b})"

@describe.register
def _(value: str):
    return f"String of length {len(value)}: {value!r}"

@describe.register
def _(value: list):
    return f"List with {len(value)} items"

print(describe(42))         # Integer: 42 (binary: 101010)
print(describe("hello"))    # String of length 5: 'hello'
print(describe([1, 2, 3]))  # List with 3 items
print(describe(3.14))       # Unknown type: 3.14  (default)
Commonly confused
lru_cache on methods can leak memory. Applying @lru_cache to an instance method keeps a reference to self in the cache, preventing the instance from being garbage collected. For methods, consider functools.cached_property (for properties) or a different caching strategy.
reduce is often not the clearest choice. Python's creator deliberately moved reduce out of builtins into functools, encouraging explicit loops or sum()/math.prod()/any()/all() instead. Use reduce only when the operation genuinely doesn't have a clearer built-in.

✅ Intermediate tab complete

  • I can use partial() to fix some arguments and create a specialised function
  • I can use reduce() to combine a sequence into a single value
  • I know to prefer sum()/math.prod() over reduce() when they apply
  • I can use @singledispatch for type-based overloading

Continue to csv →

🔴 Expert

cached_property, total_ordering, and LRU internals

What you'll learn: @cached_property for compute-once instance attributes. @total_ordering to generate all comparison methods from two. How lru_cache implements O(1) LRU eviction with a linked list and dict.

How to read this tab: Implement a class with @cached_property for an expensive computed attribute. Note that it computes once per instance, then caches.

⏱ 25 min📄 1 section🔶 Prerequisite: After Stage 2

cached_property, total_ordering, and the wraps protocol

functools includes several class-oriented tools. @cached_property computes a property once and caches it on the instance. @total_ordering generates all comparison methods from just __eq__ and one other. @wraps (covered fully on the Decorators page) copies the wrapped function's metadata.

Pythonfunctools_advanced.py
from functools import cached_property, total_ordering

class Dataset:
    def __init__(self, data):
        self.data = data

    @cached_property
    def statistics(self):
        """Computed once, then cached on the instance."""
        print("Computing statistics...")
        import statistics
        return {
            "mean": statistics.mean(self.data),
            "stdev": statistics.stdev(self.data),
        }

ds = Dataset([1, 2, 3, 4, 5])
print(ds.statistics)   # Computing statistics... → {...}
print(ds.statistics)   # {...}  — no recomputation, cached on instance

# @total_ordering — define __eq__ and one comparison, get the rest free
@total_ordering
class Version:
    def __init__(self, major, minor):
        self.major, self.minor = major, minor
    def __eq__(self, other):
        return (self.major, self.minor) == (other.major, other.minor)
    def __lt__(self, other):
        return (self.major, self.minor) < (other.major, other.minor)
    # total_ordering generates __le__, __gt__, __ge__ automatically

v1 = Version(1, 2)
v2 = Version(1, 5)
print(v1 < v2)    # True
print(v1 >= v2)   # False — generated from __eq__ and __lt__
print(v1 <= v2)   # True  — generated

The implementation of lru_cache uses a circular doubly-linked list plus a dict to implement the Least-Recently-Used eviction policy in O(1) per access. When the cache reaches maxsize, the least recently used entry is evicted. With maxsize=None (or @cache), no eviction occurs — the cache grows without bound, so use it only when the set of distinct argument combinations is finite.

✅ Expert tab complete

  • I can use @cached_property for a compute-once instance attribute
  • I can use @total_ordering to generate comparison methods from __eq__ and __lt__
  • I know lru_cache uses a doubly-linked list + dict for O(1) LRU eviction

Continue to csv →

Sources

1
Python Standard Library — functools. docs.python.org/3/library/functools.html.
2
PEP 443 — Single-dispatch generic functions. peps.python.org/pep-0443/.
3
Python Standard Library — functools.lru_cache and cache. docs.python.org/3/library/functools.html#functools.lru_cache.
4
Python Standard Library — functools.cached_property. docs.python.org/3/library/functools.html#functools.cached_property.
Source confidence: High Last verified: Primary source: docs.python.org functools