Skip to content

functools — The Functional Toolkit

Python’s functools module is the standard library’s collection of higher-order function utilities, tools that work with, modify, or enhance other functions.

While map(), filter(), and reduce() handle data transformation, functools handles function transformation: fixing arguments, caching results, generating boilerplate, and enabling dispatch based on type.

The module covers four broad concerns:

  • Argument binding — fix some arguments of a function ahead of time (partial)
  • Memoization — cache results to avoid recomputation (lru_cache, cache)
  • Class utilities — reduce boilerplate in class definitions (total_ordering)
  • Type dispatch — route function calls based on argument type (singledispatch)

partial creates a new function with some arguments pre-filled. It is useful when you have a general function but need a specialised version of it for a specific context.

from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2) # fix exponent=2
cube = partial(power, exponent=3) # fix exponent=3
print(square(5)) # 25
print(cube(3)) # 27
# natural fit with map — no lambda needed
numbers = [1, 2, 3, 4, 5]
print(list(map(square, numbers))) # [1, 4, 9, 16, 25]

A practical example — a logger with a fixed level:

from functools import partial
def log(level, message):
print(f"[{level}] {message}")
info = partial(log, "INFO")
error = partial(log, "ERROR")
info("Server started") # [INFO] Server started
error("Connection refused") # [ERROR] Connection refused

Both decorators cache the results of a function call so repeated calls with the same arguments return instantly from the cache. lru_cache has a configurable size limit, while cache is equivalent to lru_cache(maxsize=None) — unlimited.

from functools import lru_cache, cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
@cache # unlimited cache
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
print(fibonacci(50)) # instant — results cached
print(fibonacci.cache_info()) # CacheInfo(hits=48, misses=51, ...)

total_ordering — Fill in Comparison Methods

Section titled “total_ordering — Fill in Comparison Methods”

Defining all six comparison methods (__eq__, __lt__, __le__, __gt__, __ge__, __ne__) for a class is tedious and repetitive. total_ordering lets you define just __eq__ and one of the ordering methods, and fills in the rest automatically.

from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor, patch):
self.major = major
self.minor = minor
self.patch = patch
def __eq__(self, other):
return (self.major, self.minor, self.patch) == \
(other.major, other.minor, other.patch)
def __lt__(self, other): # define ONE ordering method
return (self.major, self.minor, self.patch) < \
(other.major, other.minor, other.patch)
# __le__, __gt__, __ge__ are auto-generated by @total_ordering
v1 = Version(1, 2, 0)
v2 = Version(1, 3, 0)
print(v1 < v2) # True
print(v1 > v2) # False ← auto-generated
print(v1 <= v2) # True ← auto-generated
print(sorted([Version(2, 0, 0), Version(1, 0, 0), Version(1, 5, 0)]))

singledispatch — Function Overloading by Type

Section titled “singledispatch — Function Overloading by Type”

singledispatch allows a single function name to have different implementations depending on the type of the first argument — similar to method overloading in statically typed languages, but resolved at runtime.

from functools import singledispatch
@singledispatch
def process(value):
raise TypeError(f"Unsupported type: {type(value)}")
@process.register(int)
def _(value):
return f"Integer: {value * 2}"
@process.register(str)
def _(value):
return f"String: {value.upper()}"
@process.register(list)
def _(value):
return f"List of {len(value)} items"
print(process(42)) # Integer: 84
print(process("hello")) # String: HELLO
print(process([1, 2, 3])) # List of 3 items

The base function decorated with @singledispatch acts as the fallback for any unregistered type. Each @process.register adds a specialised implementation for a specific type.