Python functools: lru_cache, partial, reduce, and wraps

The functools module is one of Python’s most practical standard-library gems. It ships a small set of higher-order function utilities — tools that operate on or return other functions — that eliminate boilerplate, improve performance, and make your code easier to reason about. You don’t need to install anything; import functools is enough.

🎁 Free: AI Publishing Checklist — 7 steps in Python · Full pipeline: germy5.gumroad.com/l/xhxkzz (pay what you want, min $9.99)

lru_cache — Stop Paying Twice for the Same Result

lru_cache (Least Recently Used cache) wraps a function and remembers its return values. Call it with the same arguments twice, and the second call returns the stored result instantly.

import functools
import time

@functools.lru_cache(maxsize=128)
def fetch_article_ids(api_key: str) -> tuple[str, ...]:
    """Fetch already-published article IDs from the Dev.to API."""
    time.sleep(1)          # simulates a real HTTP call
    return ("abc123", "def456", "ghi789")

# First call: hits the network
ids = fetch_article_ids("my-api-key")   # ~1 s

# Second call: instant — result came from cache
ids = fetch_article_ids("my-api-key")   # 0 ms

maxsize=128 keeps up to 128 distinct argument combinations. Set it to None (or use @functools.cache, available since Python 3.9) for an unbounded cache.

Inspect the cache:

print(fetch_article_ids.cache_info())
# CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)

When NOT to cache: arguments must be hashable (no lists, dicts). If your function receives mutable input or has side effects you need every time (sending an email, writing a file), skip the cache.

Real pipeline example: In a publishing automation script, fetch_article_ids is called once at startup and potentially again inside a loop. With @lru_cache, the HTTP round-trip happens exactly once per process run regardless of how many times the function is called.

partial — Freeze Some Arguments Up Front

functools.partial creates a new callable with some arguments pre-filled. Think of it as a lightweight factory that adapts a general function to a specific context.

import functools
import requests

# General function
def api_get(url: str, headers: dict, timeout: int = 10):
    return requests.get(url, headers=headers, timeout=timeout)

# Pre-configured version with auth headers frozen in
devto_get = functools.partial(
    api_get,
    headers={"api-key": "my-secret-key", "Accept": "application/json"},
    timeout=15,
)

# Now every call needs only the URL
response = devto_get("https://dev.to/api/articles/me/published")

Before partial you would either duplicate the headers dict everywhere or write a wrapper function. partial gives you a clean, named callable with zero extra code.

Another classic use: adapting a two-argument function for sorted or map:

from functools import partial

def multiply(x, factor):
    return x * factor

double = partial(multiply, factor=2)
print(list(map(double, [1, 2, 3, 4])))   # [2, 4, 6, 8]

wraps — Every Decorator Must Use This

When you write a decorator, Python replaces the original function with your wrapper. Without wraps, tools like help(), inspect.signature(), and logging lose the original name and docstring.

Without wraps — broken:

def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling something...")
        return func(*args, **kwargs)
    return wrapper

@log_call
def publish_article(title: str) -> bool:
    """Publish an article and return True on success."""
    ...

print(publish_article.__name__)   # "wrapper"  ← wrong
print(publish_article.__doc__)    # None        ← wrong

With wraps — correct:

import functools

def log_call(func):
    @functools.wraps(func)          # ← one line fixes everything
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}...")
        return func(*args, **kwargs)
    return wrapper

@log_call
def publish_article(title: str) -> bool:
    """Publish an article and return True on success."""
    ...

print(publish_article.__name__)   # "publish_article"  ✓
print(publish_article.__doc__)    # "Publish an article..."  ✓

wraps copies __name__, __qualname__, __doc__, __dict__, __module__, and __wrapped__. It costs one decorator line and has no downsides — use it every time you write a wrapper.

reduce — Fold a Sequence to a Single Value

functools.reduce applies a two-argument function cumulatively across an iterable, collapsing it to a single value.

from functools import reduce

numbers = [1, 2, 3, 4, 5]
total = reduce(lambda acc, x: acc + x, numbers)   # 15

It is less common now that sum(), max(), min(), and comprehensions cover most everyday cases. But reduce shines when you need to chain function calls dynamically:

from functools import reduce

def apply_pipeline(value, transforms):
    """Apply a list of functions to a value in sequence."""
    return reduce(lambda v, fn: fn(v), transforms, value)

pipeline = [str.strip, str.lower, lambda s: s.replace(" ", "-")]
slug = apply_pipeline("  Hello World  ", pipeline)
print(slug)   # "hello-world"

This pattern — a list of single-argument functions collapsed with reduce — is a clean way to build configurable text-processing pipelines without nested calls or temporary variables.

total_ordering — Define Two Comparisons, Get Six

If you have a class that needs ordering (e.g., articles sorted by publish date), you normally must implement __eq__, __lt__, __le__, __gt__, and __ge__. total_ordering lets you define just __eq__ and one of the four inequality methods, and fills in the rest automatically.

from functools import total_ordering

@total_ordering
class Article:
    def __init__(self, title: str, word_count: int):
        self.title = title
        self.word_count = word_count

    def __eq__(self, other):
        return self.word_count == other.word_count

    def __lt__(self, other):
        return self.word_count < other.word_count

a = Article("Short post", 300)
b = Article("Deep dive", 1200)

print(a < b)    # True
print(a >= b)   # False  ← generated automatically
print(a <= b)   # True   ← generated automatically

The trade-off: auto-generated methods are slightly slower than hand-written ones. For classes compared millions of times in tight loops, implement all six manually. For most domain objects, total_ordering is fine.

cached_property — Compute Once Per Instance

functools.cached_property (Python 3.8+) works like @property but stores the result on the instance after the first access. Subsequent accesses read from __dict__ directly, bypassing the descriptor entirely.

import functools

class ArticleStats:
    def __init__(self, words: list[str]):
        self.words = words

    @functools.cached_property
    def word_count(self) -> int:
        print("Computing word count...")
        return len(self.words)

stats = ArticleStats("the quick brown fox".split())
print(stats.word_count)   # Computing word count... → 4
print(stats.word_count)   # 4  (no print — cached on instance)

Ideal for expensive computations derived from instance data: parsing, aggregation, expensive lookups. Unlike lru_cache on a method, cached_property is per-instance and cleared when the instance is garbage-collected — no unbounded global cache growth.

Which Ones You’ll Use Every Day

Tool Frequency Reason
lru_cache / cache Very high Eliminate redundant I/O instantly
wraps Very high Required in every decorator
partial High Pre-configure functions for specific contexts
cached_property Medium Lazy, once-per-instance properties
total_ordering Medium Clean comparison support on data classes
reduce Low-niche Useful for dynamic pipelines; otherwise use builtins

Start with lru_cache and wraps — they pay off immediately and show up in production codebases constantly. Add partial when you find yourself repeating the same keyword arguments. Reach for reduce only when you genuinely have a list of functions to chain.

If you’re building automation pipelines in Python and want a complete end-to-end example — from script structure to publishing — check out the full pipeline guide at germy5.gumroad.com/l/xhxkzz.

Further Reading

If this was useful, the ❤️ button helps other developers find it.

Building a Python content pipeline? I sell the complete automation system as a one-time download — Dev.to API, Claude API, launchd, Gumroad. Check it out ($9.99)

Leave a Reply