functools provides tools for working with functions. Here are the most useful ones.

lru_cache

Memoize function results:

from functools import lru_cache
 
@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)
 
fibonacci(100)  # Instant, not 2^100 calls
 
# Check cache stats
fibonacci.cache_info()
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
 
# Clear cache
fibonacci.cache_clear()

For unbounded cache:

@lru_cache(maxsize=None)
def expensive_computation(x):
    ...

cache (Python 3.9+)

Simpler unbounded cache:

from functools import cache
 
@cache
def factorial(n):
    return n * factorial(n-1) if n else 1

cached_property (Python 3.8+)

Cache property on first access:

from functools import cached_property
 
class DataAnalyzer:
    def __init__(self, data):
        self.data = data
    
    @cached_property
    def statistics(self):
        # Expensive computation, done once
        return {
            "mean": sum(self.data) / len(self.data),
            "max": max(self.data),
            "min": min(self.data),
        }

partial

Pre-fill function arguments:

from functools import partial
 
def power(base, exponent):
    return base ** exponent
 
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
 
square(5)  # 25
cube(3)    # 27
 
# Useful with callbacks
import json
pretty_json = partial(json.dumps, indent=2, sort_keys=True)
print(pretty_json({"b": 2, "a": 1}))

partialmethod

Like partial, but for methods:

from functools import partialmethod
 
class Cell:
    def __init__(self):
        self.alive = False
    
    def set_state(self, state):
        self.alive = state
    
    # Create convenience methods
    set_alive = partialmethod(set_state, True)
    set_dead = partialmethod(set_state, False)
 
cell = Cell()
cell.set_alive()  # cell.alive = True

reduce

Reduce iterable to single value:

from functools import reduce
 
# Sum (use sum() instead in practice)
reduce(lambda x, y: x + y, [1, 2, 3, 4])  # 10
 
# Product
reduce(lambda x, y: x * y, [1, 2, 3, 4])  # 24
 
# With initial value
reduce(lambda x, y: x + y, [1, 2, 3], 10)  # 16
 
# Flatten nested list
nested = [[1, 2], [3, 4], [5, 6]]
reduce(lambda x, y: x + y, nested)  # [1, 2, 3, 4, 5, 6]
 
# Find max (use max() instead)
reduce(lambda x, y: x if x > y else y, [3, 1, 4, 1, 5])  # 5

wraps

Preserve function metadata in decorators:

from functools import wraps
 
def my_decorator(func):
    @wraps(func)  # Copies __name__, __doc__, etc.
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper
 
@my_decorator
def greet(name):
    """Greet someone."""
    return f"Hello, {name}!"
 
greet.__name__  # "greet" (not "wrapper")
greet.__doc__   # "Greet someone."

singledispatch

Overload functions by type:

from functools import singledispatch
 
@singledispatch
def process(data):
    raise NotImplementedError(f"Cannot process {type(data)}")
 
@process.register
def _(data: str):
    return f"String: {data.upper()}"
 
@process.register
def _(data: int):
    return f"Integer: {data * 2}"
 
@process.register
def _(data: list):
    return f"List with {len(data)} items"
 
process("hello")  # "String: HELLO"
process(5)        # "Integer: 10"
process([1, 2])   # "List with 2 items"

For methods, use singledispatchmethod:

from functools import singledispatchmethod
 
class Processor:
    @singledispatchmethod
    def process(self, data):
        raise NotImplementedError()
    
    @process.register
    def _(self, data: str):
        return data.upper()

total_ordering

Auto-generate comparison methods:

from functools import total_ordering
 
@total_ordering
class Version:
    def __init__(self, major, minor):
        self.major = major
        self.minor = 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)
 
# Now has __le__, __gt__, __ge__ automatically
v1 = Version(1, 0)
v2 = Version(2, 0)
v1 < v2   # True
v1 <= v2  # True (auto-generated)
v1 >= v2  # False (auto-generated)

cmp_to_key

Convert old-style comparison function:

from functools import cmp_to_key
 
def compare_length(a, b):
    return len(a) - len(b)
 
words = ["apple", "pie", "banana"]
sorted(words, key=cmp_to_key(compare_length))
# ['pie', 'apple', 'banana']

Practical Examples

Memoized API calls

from functools import lru_cache
 
@lru_cache(maxsize=100)
def fetch_user(user_id):
    # Expensive API call
    response = requests.get(f"/api/users/{user_id}")
    return response.json()

Configuration with defaults

from functools import partial
 
def send_email(to, subject, body, from_addr="noreply@example.com"):
    ...
 
# Pre-configured sender
send_notification = partial(send_email, from_addr="alerts@example.com")

Type-based serialization

from functools import singledispatch
import json
from datetime import datetime
 
@singledispatch
def serialize(obj):
    return str(obj)
 
@serialize.register
def _(obj: datetime):
    return obj.isoformat()
 
@serialize.register
def _(obj: dict):
    return {k: serialize(v) for k, v in obj.items()}

Quick Reference

FunctionUse Case
lru_cacheMemoize function results
cacheUnbounded memoization
cached_propertyCache property on first access
partialPre-fill function arguments
reduceReduce iterable to value
wrapsPreserve function metadata
singledispatchOverload by argument type
total_orderingGenerate comparison methods

functools makes functional patterns practical in Python. Use it to write cleaner, more efficient code.

React to this post: