Python’s functools module is one of the most underused yet powerful standard library modules. In 2026, functools provides tools for higher-order functions, memoization, partial application, and function composition that eliminate boilerplate and make code more expressive. This complete guide covers every functools function with practical examples.
📋 Table of Contents
functools.lru_cache and cache
from functools import lru_cache, cache
import time
# Simple memoization — cache indefinitely
@cache # Python 3.9+, same as lru_cache(maxsize=None)
def fibonacci(n: int) -> int:
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# LRU cache — keep only most recent N results
@lru_cache(maxsize=128)
def expensive_api_call(user_id: int) -> dict:
time.sleep(0.5) # simulate slow API
return {"id": user_id, "name": "User"}
# Cache info and management
print(fibonacci.cache_info()) # CacheInfo(hits=X, misses=Y, maxsize=None, currsize=N)
fibonacci.cache_clear()
# Method-level cache (instance-level, not shared across instances)
class DataService:
@lru_cache(maxsize=256)
def get_user(self, user_id: int) -> dict:
return fetch_from_db(user_id)
functools.partial — Partial Application
from functools import partial
import operator
# Create a new function with some args pre-filled
def power(base: float, exponent: float) -> float:
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25.0
print(cube(3)) # 27.0
# Useful with map/filter
double = partial(operator.mul, 2)
numbers = [1, 2, 3, 4, 5]
doubled = list(map(double, numbers)) # [2, 4, 6, 8, 10]
# Partial with keyword args
import requests
get_from_api = partial(requests.get, headers={"Authorization": "Bearer token123"})
response = get_from_api("https://api.example.com/users")
# In configuration/dependency injection
from functools import partial
def create_user(db, email: str, role: str = "user") -> dict:
return db.insert("users", {"email": email, "role": role})
create_admin = partial(create_user, db=prod_db, role="admin")
admin = create_admin("admin@example.com")
functools.reduce
from functools import reduce
import operator
# Sum a list (use sum() instead in practice)
total = reduce(operator.add, [1, 2, 3, 4, 5]) # 15
# Product of list (no built-in)
product = reduce(operator.mul, [1, 2, 3, 4, 5]) # 120
# Flatten nested list
nested = [[1, 2], [3, 4], [5, 6]]
flat = reduce(operator.add, nested) # [1, 2, 3, 4, 5, 6]
# Compose functions (right to left)
def compose(*functions):
return reduce(lambda f, g: lambda x: f(g(x)), functions)
pipeline = compose(str.upper, str.strip, str.lower)
print(pipeline(" Hello World ")) # "HELLO WORLD"
# Deep dictionary merge
dicts = [{"a": 1}, {"b": 2, "c": 3}, {"d": 4}]
merged = reduce(lambda acc, d: {**acc, **d}, dicts) # {"a":1,"b":2,"c":3,"d":4}
functools.wraps
from functools import wraps
import time, logging
logger = logging.getLogger(__name__)
def log_calls(func):
@wraps(func) # preserves __name__, __doc__, __wrapped__, __annotations__
def wrapper(*args, **kwargs):
logger.info(f"Calling {func.__name__}")
result = func(*args, **kwargs)
logger.info(f"Done {func.__name__}")
return result
return wrapper
def measure(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__}: {elapsed*1000:.2f}ms")
return result
return wrapper
@log_calls
@measure
def process_data(data: list) -> list:
return sorted(data)
# Without @wraps, process_data.__name__ would be "wrapper"
print(process_data.__name__) # "process_data" (correct!)
functools.total_ordering
from functools import total_ordering
@total_ordering # generates __le__, __lt__, __ge__, __gt__ from __eq__ and one comparison
class Version:
def __init__(self, major: int, minor: int, patch: int = 0):
self.major = major
self.minor = minor
self.patch = patch
def __eq__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
def __lt__(self, other: "Version") -> bool:
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
def __repr__(self) -> str:
return f"Version({self.major}.{self.minor}.{self.patch})"
versions = [Version(2, 0), Version(1, 9, 5), Version(3, 0), Version(1, 9, 4)]
print(sorted(versions)) # [1.9.4, 1.9.5, 2.0, 3.0]
print(Version(1, 9) < Version(2, 0)) # True
print(Version(3, 0) >= Version(2, 1)) # True (generated!)
functools.singledispatch — Method Overloading
from functools import singledispatch
@singledispatch
def process(obj):
raise TypeError(f"Cannot process {type(obj)}")
@process.register(int)
def _(obj: int) -> str:
return f"Integer: {obj}"
@process.register(str)
def _(obj: str) -> str:
return f"String: '{obj}'"
@process.register(list)
def _(obj: list) -> str:
return f"List with {len(obj)} items"
@process.register(dict)
def _(obj: dict) -> str:
return f"Dict with {len(obj)} keys: {list(obj.keys())}"
print(process(42)) # "Integer: 42"
print(process("hello")) # "String: 'hello'"
print(process([1, 2, 3])) # "List with 3 items"
# Useful for serialization/display of varied types without isinstance chains
functools.cached_property
from functools import cached_property
import statistics
class Dataset:
def __init__(self, data: list[float]):
self._data = data
@cached_property
def mean(self) -> float:
print("Computing mean...")
return statistics.mean(self._data)
@cached_property
def stdev(self) -> float:
print("Computing stdev...")
return statistics.stdev(self._data)
@cached_property
def sorted_data(self) -> list[float]:
print("Sorting...")
return sorted(self._data)
ds = Dataset([3, 1, 4, 1, 5, 9, 2, 6])
print(ds.mean) # "Computing mean..." then value
print(ds.mean) # value directly (cached)
print(ds.stdev) # "Computing stdev..." then value
Python functools in 2026 is essential for clean, efficient code. Use @cache for memoization, partial() for configuration injection, @wraps for decorator transparency, @total_ordering for comparison classes, @singledispatch for type-based dispatch, and cached_property for lazy computed attributes. These tools replace boilerplate and clarify intent.
📚 You might also like
🔗 Share this article




✍️ Leave a Comment