Skip to main content
The functools module provides higher-order functions and operations on callable objects.

Module Import

import functools
from functools import lru_cache, wraps, partial

Caching Decorators

@lru_cache - LRU Cache

Least Recently Used cache decorator for memoization.
from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

# Much faster with caching
print(fibonacci(100))  # Instant!

# Check cache statistics
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)

# Clear cache
fibonacci.cache_clear()

@cache - Unlimited Cache

Simple unbounded cache (Python 3.9+).
from functools import cache

@cache
def expensive_function(n):
    print(f"Computing {n}...")
    return n * n

print(expensive_function(5))  # Computing 5... 25
print(expensive_function(5))  # 25 (cached, no print)

Function Wrappers

@wraps - Preserve Function Metadata

Preserves original function’s metadata when creating wrappers.
from functools import wraps

def my_decorator(func):
    @wraps(func)  # Preserves func's metadata
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name):
    """Greet someone by name"""
    return f"Hello, {name}!"

print(greet.__name__)  # 'greet' (not 'wrapper')
print(greet.__doc__)   # 'Greet someone by name'

Partial Functions

partial() - Partial Application

Create new function with some arguments pre-filled.
from functools import partial

# Regular function
def power(base, exponent):
    return base ** exponent

# Create partial functions
square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(5))  # 25
print(cube(5))    # 125

# Practical use: pre-configure functions
from operator import mul
double = partial(mul, 2)
triple = partial(mul, 3)

print(double(10))  # 20
print(triple(10))  # 30

partialmethod() - Partial for Methods

from functools import partialmethod

class Cell:
    def __init__(self):
        self.value = 0
    
    def set_value(self, value, multiplier=1):
        self.value = value * multiplier
    
    # Create partial method
    set_double = partialmethod(set_value, multiplier=2)
    set_triple = partialmethod(set_value, multiplier=3)

cell = Cell()
cell.set_double(5)  # value = 10
cell.set_triple(5)  # value = 15

Function Composition

reduce() - Reduce Iterable

Apply function cumulatively to items.
from functools import reduce
from operator import add, mul

# Sum all numbers
numbers = [1, 2, 3, 4, 5]
total = reduce(add, numbers)
print(total)  # 15

# Product of all numbers
product = reduce(mul, numbers)
print(product)  # 120

# With initial value
total = reduce(add, numbers, 10)
print(total)  # 25 (10 + 1 + 2 + 3 + 4 + 5)

# Custom function
max_value = reduce(lambda a, b: a if a > b else b, numbers)
print(max_value)  # 5

Comparison Functions

@total_ordering - Complete Comparison Methods

Automatically provides all comparison methods from a subset.
from functools import total_ordering

@total_ordering
class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
    
    def __eq__(self, other):
        return self.grade == other.grade
    
    def __lt__(self, other):
        return self.grade < other.grade
    
    # __le__, __gt__, __ge__ are automatically provided!

s1 = Student('Alice', 90)
s2 = Student('Bob', 85)

print(s1 > s2)   # True
print(s1 >= s2)  # True
print(s1 <= s2)  # False

cmp_to_key() - Convert Comparison Function

Convert old-style comparison function to key function.
from functools import cmp_to_key

def compare(a, b):
    """Old-style comparison function"""
    if a < b:
        return -1
    elif a > b:
        return 1
    return 0

# Use with sorted()
items = [5, 2, 8, 1, 9]
sorted_items = sorted(items, key=cmp_to_key(compare))
print(sorted_items)  # [1, 2, 5, 8, 9]

Single Dispatch

@singledispatch - Generic Functions

Create generic functions that behave differently based on argument type.
from functools import singledispatch

@singledispatch
def process(arg):
    """Default implementation"""
    print(f"Processing {arg} as object")

@process.register(int)
def _(arg):
    print(f"Processing {arg} as integer")

@process.register(str)
def _(arg):
    print(f"Processing '{arg}' as string")

@process.register(list)
def _(arg):
    print(f"Processing list with {len(arg)} items")

# Different behavior based on type
process(42)         # Processing 42 as integer
process("hello")    # Processing 'hello' as string
process([1, 2, 3])  # Processing list with 3 items
process(3.14)       # Processing 3.14 as object

@singledispatchmethod - For Class Methods

from functools import singledispatchmethod

class Processor:
    @singledispatchmethod
    def process(self, arg):
        print(f"Processing {arg}")
    
    @process.register(int)
    def _(self, arg):
        print(f"Processing integer: {arg * 2}")
    
    @process.register(str)
    def _(self, arg):
        print(f"Processing string: {arg.upper()}")

p = Processor()
p.process(42)       # Processing integer: 84
p.process("hello")  # Processing string: HELLO

Cached Properties

@cached_property - Cache Property Value

from functools import cached_property

class DataSet:
    def __init__(self, data):
        self._data = data
    
    @cached_property
    def processed_data(self):
        """Expensive computation, cached after first access"""
        print("Processing data...")
        return [x * 2 for x in self._data]

ds = DataSet([1, 2, 3, 4, 5])
print(ds.processed_data)  # Processing data... [2, 4, 6, 8, 10]
print(ds.processed_data)  # [2, 4, 6, 8, 10] (cached, no print)

Practical Examples

Memoization for Expensive Functions

from functools import lru_cache
import time

@lru_cache(maxsize=None)
def expensive_computation(n):
    """Simulate expensive computation"""
    time.sleep(1)
    return n * n

start = time.time()
print(expensive_computation(5))  # Takes 1 second
print(expensive_computation(5))  # Instant (cached)
print(f"Time: {time.time() - start:.2f}s")  # ~1 second

Retry Decorator

from functools import wraps
import time

def retry(max_attempts=3, delay=1):
    """Retry decorator with configurable attempts and delay"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts += 1
                    if attempts >= max_attempts:
                        raise
                    print(f"Attempt {attempts} failed, retrying...")
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2)
def unstable_function():
    import random
    if random.random() < 0.7:
        raise Exception("Random failure")
    return "Success!"

Function Pipeline

from functools import reduce

def compose(*functions):
    """Compose multiple functions into pipeline"""
    return reduce(lambda f, g: lambda x: f(g(x)), functions, lambda x: x)

# Define transformations
def add_10(x): return x + 10
def multiply_2(x): return x * 2
def subtract_5(x): return x - 5

# Create pipeline
pipeline = compose(subtract_5, multiply_2, add_10)

result = pipeline(5)  # ((5 + 10) * 2) - 5 = 25
print(result)

Timing Decorator

from functools import wraps
import time

def timer(func):
    """Measure function execution time"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.4f} seconds")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "Done"

slow_function()  # slow_function took 1.0001 seconds

Currying with partial

from functools import partial

def volume(length, width, height):
    return length * width * height

# Create specialized functions
cube_volume = partial(partial(volume, width=1), height=1)
box_volume = partial(volume, width=10, height=5)

print(cube_volume(length=5))  # 5
print(box_volume(length=2))   # 100

Best Practices

Always use @wraps for decorators:
from functools import wraps

def my_decorator(func):
    @wraps(func)  # Preserves metadata
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
Use @lru_cache for expensive pure functions:
@lru_cache(maxsize=128)
def expensive_function(arg):
    # Must be pure (same input = same output)
    return complex_calculation(arg)
Don’t cache functions with mutable arguments:
# Bad - lists aren't hashable
@lru_cache
def process(items: list):  # Will fail!
    return sum(items)

# Good - use tuple
@lru_cache
def process(items: tuple):
    return sum(items)

Complete Function List

  • @lru_cache(maxsize=128) - LRU cache with size limit
  • @cache - Unlimited cache (Python 3.9+)
  • @cached_property - Cache property value
  • @wraps(wrapped) - Update wrapper function
  • partial(func, /, *args, **keywords) - Partial application
  • partialmethod(func, /, *args, **keywords) - Partial for methods
  • reduce(function, iterable, initializer) - Cumulative application
  • @total_ordering - Fill in comparison methods
  • cmp_to_key(func) - Convert comparison function
  • @singledispatch - Single-dispatch generic function
  • @singledispatchmethod - Single-dispatch for methods

itertools

Iterator building blocks

operator

Function equivalents of operators

Built-in Functions

Core Python functions

Build docs developers (and LLMs) love