Partial Functions & Caching
Apply functools.partial to fix function arguments and use caching to speed up repeated calls — a practical Python skill.
Focus: apply functools.partial and function caching
Have you ever written a function that always passes the same argument value—like print('Warning:', message) in ten different places? Or noticed a slow function recomputing the same result every time it's called with identical inputs? Python's functools module gives you two clean solutions: partial to pre-fill arguments, and lru_cache to memoize return values. This lesson shows you how to apply functools.partial and function caching to write less code and gain serious performance.
The problem this lesson solves
Repeating yourself in arguments bloats code and invites typos. A logger call like log('INFO', message) scattered through a module forces you to update every invocation if the log level changes. Without caching, expensive functions—database queries, API calls, or heavy computations—run the same logic over and over, wasting CPU cycles and response time. Both problems degrade maintainability and speed. functools.partial and lru_cache fix these without altering your original function's logic.
Core concept / mental model
Think of partial as a function factory. You give it an existing function and some fixed arguments; it returns a new function with those arguments already supplied. It's like ordering a coffee: instead of saying "large, black, no sugar" every time, you create a "default coffee" that remembers those choices. The new function still accepts any remaining arguments.
Function caching (memoization) remembers the outputs for specific inputs. functools.lru_cache wraps any function store return values in a dictionary, keyed by arguments. If the function is called again with the same arguments, the cached result is returned instantly—no re-execution. Image it as a notepad beside your desk; you write down answers to hard questions so you don't recalculate them.
Both tools live in the standard library—no extra install needed.
How it works step by step
Step 1: Import functools
import functools
Step 2: Create a partial function
Use functools.partial(func, *args, **kwargs) to fix some arguments. The returned partial object is callable.
def power(base, exponent):
return base ** exponent
square = functools.partial(power, exponent=2)
cube = functools.partial(power, exponent=3)
print(square(5)) # 25
print(cube(5)) # 125
Step 3: Apply @lru_cache to a function
Decorate a function with @functools.lru_cache(maxsize=128). The cache stores results for up to maxsize different calls. When the cache is full, the least recently used entry is evicted.
@functools.lru_cache(maxsize=32)
def fibonacci(n):
"""Return the nth Fibonacci number (naive recursion)."""
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# First call computes; second call returns cached
print(fibonacci(10)) # 55
print(fibonacci(10)) # 55 (from cache)
Step 4: Cache statistics and clearing
fibonacci.cache_info() shows hits, misses, maxsize, and currsize. fibonacci.cache_clear() wipes the cache manually.
print(fibonacci.cache_info())
# CacheInfo(hits=8, misses=11, maxsize=32, currsize=11)
Pro tip: Use
@functools.lru_cacheonly on pure functions—functions that return the same output for the same input and have no side effects. Caching a function that prints, writes to disk, or depends on global state will give you stale or wrong results.
Hands-on walkthrough
Example 1: Pre-fill a logging function
import functools
import logging
logging.basicConfig(level=logging.INFO)
def log(level, message):
logging.log(level, message)
info_log = functools.partial(log, logging.INFO)
error_log = functools.partial(log, logging.ERROR)
info_log("Service started") # Logs INFO: Service started
error_log("Connection timeout") # Logs ERROR: Connection timeout
Output (console):
INFO:root:Service started
ERROR:root:Connection timeout
Example 2: Cache expensive computation
import functools
import time
def expensive_computation(x):
"""Simulate a slow operation (e.g., model inference)."""
time.sleep(2)
return x * x
cached_computation = functools.lru_cache(maxsize=10)(expensive_computation)
start = time.time()
print(cached_computation(4)) # 16 (2s delay)
print("Time:", time.time() - start)
start = time.time()
print(cached_computation(4)) # 16 (instant)
print("Time:", time.time() - start)
Output (approximate):
16
Time: 2.001
16
Time: 0.000
Example 3: Combine partial and lru_cache
import functools
def multiply(a, b):
return a * b
double = functools.partial(multiply, 2)
double = functools.lru_cache(maxsize=None)(double)
print(double(10)) # 20 (from cache on second call)
print(double(10)) # 20 (from cache)
Compare options / when to choose what
| Tool | Purpose | When to Use | Side effects? |
|---|---|---|---|
functools.partial |
Pre-fill arguments | You call the same function with constant arguments at many places | No (new function object) |
functools.lru_cache |
Cache return values | Function is pure, called repeatedly with identical args, and is expensive | No (if pure) |
| Manual dict caching | Arbitrary caching logic | Need full control over cache key, eviction, or persistence | Requires manual code |
functools.cache (Python 3.9+) |
Simpler, unbounded cache | Unbounded cache is acceptable; pure functions | No |
Key distinction: partial does not cache—it only fixes arguments. Caching (with lru_cache or cache) speeds up repeated calls. Use them together for a perfect combination: a partially-applied function that also memoizes its results.
Troubleshooting & edge cases
-
Mutable default arguments in cached functions:
lru_cacheuses argument values to build the cache key. Mutable objects like lists or dicts cause errors because they're unhashable.@functools.lru_cache def process(data): # data is list ... # TypeError: unhashable type: 'list'Fix: Convert mutable to immutable (e.g.,tuple(data)) inside the function or before calling. -
Cache never evicts: If
maxsize=None, the cache grows unbounded. For long-running apps, this can consume all memory. Always set a reasonablemaxsizeor use@functools.cachewhen you know caching is short-lived. -
Side effects in cached functions: A function that prints something will print only on the first call—not on cache hits. ```python @functools.lru_cache def greet(name): print(f"Hello, {name}!") return name.upper()
greet("Alice") # prints greet("Alice") # no print, returns cached ``` Fix: Keep logging side effects outside the cached function.
- Partial with keyword arguments: Order matters when mixing positional and keyword defaults.
python def f(a, b, c): pass p = functools.partial(f, 1, c=3) # a=1 is positional, b remains free p(2) # calls f(1, 2, 3)
What you learned & what's next
You now know how to apply functools.partial and function caching in Python. You can:
- Create new functions with pre-filled arguments using partial
- Speed up pure functions with lru_cache and examine cache statistics
- Combine both techniques for cleaner, faster code
- Avoid common pitfalls like unhashable arguments or side effects in cached functions
Next lesson in your Python journey: Generators and yield statements—where you'll learn to produce values on the fly without storing entire sequences in memory. This pairs perfectly with caching to build high-performance data pipelines.
Practice recap: Write a partial function that converts Celsius to Fahrenheit (formula: F = C * 9/5 + 32) with the multiplier 9/5 fixed. Then add lru_cache to remember repeated conversions. Call it with 0, 100, 0, and check cache_info(). Did you get a hit on the second call of 0?
Practice recap
Write a partial function that converts Celsius to Fahrenheit (formula: F = C * 9/5 + 32) with the multiplier 9/5 fixed. Then add lru_cache to remember repeated conversions. Call it with 0, 100, 0, and check cache_info(). Did you get a hit on the second call of 0?
Common mistakes
- Using
functools.partialwith mutable default arguments—the partial objec t will share the same mutable across calls, leading to surprising behavior. - Applying
@lru_cacheto a function that returns different values for the same input (e.g.,datetime.now()orrandom.random)—caching gives stale, incorrect results. - Forgetting that
lru_cachecaches by positional and keyword arguments as a tuple, sofunc(a=1, b=2)andfunc(b=2, a=1)are treated as different calls and don't share a cache entry. - Setting
maxsize=Nonein production without a memory limit—unbounded caching can cause MemoryError over long-running processes.
Variations
- Use
functools.cache(Python 3.9+) for unbounded caching without themaxsizeparameter—simpler but riskier. - Use
functools.lru_cachewithtyped=Trueto cache based on argument types separately (e.g.,int(5)vsfloat(5)). - Use third-party libraries like
cachetoolsfor advanced caching features (TTL, multi-key, etc.).
Real-world use cases
- Pre-fill
requests.getwith headers and base URL usingpartialto avoid repeating authentication tokens in every API call. - Cache the result of a database query function that takes a user ID—reduce latency for repeated lookups in a web server.
- Use
partialto create specialized callback functions from a general function in event-driven systems (e.g., GUI button handlers).
Key takeaways
functools.partialcreates a new callable with some arguments already supplied — perfect for reducing repetition.@functools.lru_cachememoizes pure functions, dramatically speeding up repeated calls with the same arguments.- Always use pure functions (no side effects, deterministic) with
lru_cacheto avoid incorrect caching results. - Mutable arguments (like lists) can't be cached directly; convert to immutable forms (e.g., tuple).
- Combine
partialandlru_cacheto get both cleaner function calls and performance gains. - Check cache stats with
cache_info()and clear withcache_clear()for debugging or resetting the cache.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.