Functools Deep drive: lru_cache, reduce & partial
Jul 07, 2026 5 Min Read 167 Views
(Last Updated)
Most Python developers use functions every day without thinking much about the function object itself, but Python treats functions as first-class citizens, and the functools module is built entirely around that idea.
Whether you want to cache expensive computations, build decorators that behave correctly, or write a single function that works differently for different argument types, the Python functools module has a tool for it. In this blog, we walk through the most useful functions in functools: lru_cache, reduce, partial, wraps, and singledispatch, with practical code examples for each.
Table of contents
- TL;DR Summary
- lru_cache: Automatic Memoisation
- Basic Usage
- Cache Management
- reduce: Cumulative Computation
- Basic Usage
- Using an Initial Value
- partial: Pre-Filling Function Arguments
- Basic Usage
- Real-World Use: Configuring Callbacks
- wraps: Preserving Function Metadata in Decorators
- The Problem Without wraps
- The Fix With wraps
- singledispatch: Type-Based Function Overloading
- Basic Usage
- Functools Module: Quick Reference
- Common Mistakes When Using Functools
- Conclusion
- FAQs
- What is the functools module in Python?
- What does lru_cache do in Python?
- What is the difference between reduce and sum in Python?
- What is functools.partial used for?
- Why do I need functools.wraps in decorators?
- What is functools.singledispatch?
- Can lru_cache be used with mutable arguments like lists?
TL;DR Summary
- The Python functools module is a standard library toolkit for working with functions as first-class objects.
- Its most useful tools include lru_cache for automatic memoisation, reduce for cumulative computations, partial for pre-filling function arguments, wraps for preserving metadata in decorators, and singledispatch for type-based function overloading.
- Mastering the Python functools module makes your code faster, more reusable, and significantly more idiomatic.
Want to master Python fundamentals, functional programming, data structures, and real-world projects with mentorship? Check out HCL GUVI’s Python Programming Course designed for learners who want to write efficient, idiomatic Python with hands-on practice and structured guidance.
lru_cache: Automatic Memoisation
lru_cache is a decorator that automatically caches the results of a function based on its arguments. The next time the function is called with the same arguments, the cached result is returned instantly instead of being recomputed an optimisation technique called memoisation.
Basic Usage
| from functools import lru_cache @lru_cache(maxsize=None) def fibonacci(n): if n < 2: return n return fibonacci(n – 1) + fibonacci(n – 2) print(fibonacci(50)) # Instant, despite exponential recursion |
Without lru_cache, fibonacci(50) would make over 2^50 recursive calls. With it, each unique value of n is computed only once, turning an exponential algorithm into a linear one.
Cache Management
lru_cache exposes useful introspection and control methods:
| fibonacci.cache_info() # CacheInfo(hits=48, misses=51, maxsize=None, currsize=51) fibonacci.cache_clear() # Clear the cache entirely |
The maxsize parameter controls how many results are cached. Setting maxsize=None means unlimited caching (be careful with memory usage); a numeric value uses a Least Recently Used eviction policy, discarding the oldest unused entries once the limit is reached.
Want to master Python fundamentals, functional programming, data structures, and real-world projects with mentorship? Check out HCL GUVI’s Python Programming Course designed for learners who want to write efficient, idiomatic Python with hands-on practice and structured guidance.
Python’s @lru_cache decorator works only with hashable arguments because it stores function calls as dictionary keys internally. Immutable types such as integers, strings, and tuples are supported, but passing mutable types like lists or dictionaries raises a TypeError since they are unhashable and cannot be used as cache keys.
Read More: What is Recursion in Python? A Beginner’s Guide
reduce: Cumulative Computation
reduce applies a function of two arguments cumulatively to the items of an iterable, reducing it to a single value. It is the functional programming equivalent of a running total but generalised to any binary operation.
Basic Usage
| from functools import reduce numbers = [1, 2, 3, 4, 5] # Sum all numbers total = reduce(lambda acc, x: acc + x, numbers) print(total) # 15 # Find the maximum maximum = reduce(lambda acc, x: acc if acc > x else x, numbers) print(maximum) # 5 |
Using an Initial Value
reduce accepts an optional third argument an initial value for the accumulator. This is essential when the iterable might be empty, or when you want to start from a specific value.
| # Concatenate strings with an initial value words = [‘Python’, ‘is’, ‘fun’] sentence = reduce(lambda acc, w: acc + ‘ ‘ + w, words, ‘Sentence:’) print(sentence) # ‘Sentence: Python is fun’ # Without initial value, an empty iterable raises TypeError reduce(lambda a, b: a + b, []) # TypeError: reduce() of empty sequence reduce(lambda a, b: a + b, [], 0) # 0 safe with initial value |
In practice, most simple reductions (sum, max, min, any, all) are better expressed using Python’s built-in functions sum(), max(), min() which are faster and more readable. reduce shines when your accumulation logic is custom and does not map to a built-in.
partial: Pre-Filling Function Arguments
partial creates a new function with some arguments of the original function already filled in, known as partial application. This is useful for adapting a general-purpose function to a more specific use case without writing a wrapper function manually.
Basic Usage
| from functools import partial def power(base, exponent): return base ** exponent square = partial(power, exponent=2) cube = partial(power, exponent=3) print(square(5)) # 25 print(cube(5)) # 125 |
Real-World Use: Configuring Callbacks
partial is especially common when working with APIs that expect a callback function with a fixed signature, but you need to pass extra context.
| from functools import partial def log_message(level, message): print(f‘[{level}] {message}’) # Create specialised loggers log_error = partial(log_message, ‘ERROR’) log_info = partial(log_message, ‘INFO’) log_error(‘Database connection failed’) # [ERROR] Database connection failed log_info(‘Server started successfully’) # [INFO] Server started successfully |
wraps: Preserving Function Metadata in Decorators
When you write a decorator, the wrapper function replaces the original, which means the original function’s name, docstring, and metadata are lost unless you explicitly preserve them. functools.wraps solves this in one line.
The Problem Without wraps
| def my_decorator(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @my_decorator def greet(name): ”’Returns a greeting for name.”’ return f‘Hello, {name}!’ print(greet.__name__) # ‘wrapper’ — wrong! print(greet.__doc__) # None — lost! |
The Fix With wraps
| from functools import wraps def my_decorator(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @my_decorator def greet(name): ”’Returns a greeting for name.”’ return f‘Hello, {name}!’ print(greet.__name__) # ‘greet’ — correct! print(greet.__doc__) # ‘Returns a greeting for name.’ — preserved! |
Always use @wraps when writing decorators it costs one line and prevents subtle bugs in tools that rely on function introspection, such as debuggers, documentation generators, and testing frameworks.
singledispatch: Type-Based Function Overloading
Python does not have built-in function overloading based on argument type but singledispatch from functools provides exactly that. It lets you define a generic function and register type-specific implementations.
Basic Usage
| from functools import singledispatch @singledispatch def describe(value): return f‘Generic value: {value}.’ @describe.register(int) def _(value): return f‘Integer: {value} (even={value % 2 == 0})’ @describe.register(str) def _(value): return f‘String of length {len(value)}: “{value}”‘ @describe.register(list) def _(value): return f‘List with {len(value)} items’ print(describe(42)) # Integer: 42 (even=True) print(describe(‘hello’)) # String of length 5: “hello” print(describe([1, 2, 3])) # List with 3 items |
singledispatch is widely used in serialisation libraries, formatters, and visitor-pattern implementations where behaviour needs to vary cleanly based on the type of input.
Functools Module: Quick Reference
| Function | Purpose | Common Use Case |
| lru_cache | Caches function results by argument | Speeding up recursive or expensive pure functions |
| reduce | Cumulative fold over an iterable | Custom accumulations not covered by sum/max/min |
| partial | Pre-fills function arguments | Creating specialised versions of general functions |
| wraps | Preserves metadata in decorators | Writing correct, introspectable decorators |
| singledispatch | Type-based function overloading | Generic functions with type-specific behaviour |
| cmp_to_key | Converts old-style comparator to key | Migrating legacy comparison functions for sort() |
| total_ordering | Fills in comparison methods from one | Defining custom classes with full ordering support |
Common Mistakes When Using Functools
1. Using lru_cache on functions with mutable arguments: lru_cache requires hashable arguments. Passing a list, dict, or set raises a TypeError. Convert mutable arguments to tuples or frozensets before caching, or avoid caching functions that take mutable inputs.
2. Forgetting @wraps in custom decorators: Without @wraps, the decorated function loses its name, docstring, and signature, breaking tools like help(), debuggers, and test frameworks that rely on introspection. Always add @wraps(func) to your wrapper.
3. Reaching for reduce when a built-in exists: reduce(lambda a, b: a + b, numbers) is less readable and slower than sum(numbers). Use reduce only for genuinely custom accumulation logic that built-ins like sum, max, min, any, and all cannot express.
4. Using lru_cache on methods without considering memory: Applying lru_cache to instance methods caches results keyed on self as well, which keeps instances alive as long as the cache holds a reference, potentially causing memory leaks in long-running applications. Consider cachetools or clearing the cache explicitly when instances are no longer needed.
Conclusion
The Python functools module is a small but powerful corner of the standard library that rewards the time you invest in it. lru_cache turns slow recursive functions into fast ones with a single decorator. reduce gives you a general tool for custom accumulations. partial lets you adapt general functions to specific contexts without boilerplate. wraps keeps your decorators well-behaved.
And singledispatch brings clean, type-based behaviour to generic functions. The best way to internalise these is to refactor existing code: find a recursive function and add lru_cache, find a repeated wrapper pattern and extract it into a decorator with wraps, or find a place where partial could eliminate a small helper function. Each of these small refactors will make functools feel like a natural part of your Python toolkit.
FAQs
1. What is the functools module in Python?
The functools module is part of Python’s standard library, providing higher-order functions and tools for working with functions as objects. It includes lru_cache for memoisation, reduce for cumulative computations, partial for pre-filling arguments, wraps for decorator metadata, and singledispatch for type-based overloading.
2. What does lru_cache do in Python?
lru_cache is a decorator from functools that automatically caches a function’s return values based on its arguments. Repeated calls with the same arguments return the cached result instantly instead of recomputing, which is especially powerful for recursive functions like Fibonacci.
3. What is the difference between reduce and sum in Python?
sum() is a built-in function specifically for adding numbers in an iterable, and is faster and more readable for that purpose. reduce() from functools is a general-purpose tool that applies any binary function cumulatively across an iterable, useful when your accumulation logic is not simple addition.
4. What is functools.partial used for?
.partial creates a new function with some arguments of an existing function pre-filled. It is commonly used to adapt general-purpose functions to specific contexts, such as creating specialised logging functions or configuring callbacks that need extra fixed arguments.
5. Why do I need functools.wraps in decorators?
Without functools. wraps, a decorated function loses its original name, docstring, and metadata, since the wrapper function replaces it. @wraps(func) copies this metadata onto the wrapper, ensuring tools like help(), debuggers, and documentation generators work correctly.
6. What is functools.singledispatch?
singledispatch is a decorator that enables function overloading based on the type of the first argument. You define a generic function and register type-specific implementations using @function.register(type), allowing different behaviour for different input types without if/elif type checks.
7. Can lru_cache be used with mutable arguments like lists?
No. lru_cache requires all arguments to be hashable because it uses them as dictionary keys internally. Passing a list, dict, or set raises a TypeError. Convert mutable arguments to tuples or frozensets if you need to cache based on their contents.



Did you enjoy this article?