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
- Python Decorators: Wrap Functions Without Touching Them
- Python Generators and yield: Lazy Sequences That Scale
- Python Context Managers: The with Statement Beyond File Handling
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)
Top comments (0)