- What is functools — higher-order functions for working with functions
- lru_cache — cache function results for speed
- partial — fix some arguments of a function
- reduce — reduce sequences to a single value
- wraps — preserve function metadata in decorators
What is Functools?
The functools module provides functions that work on other functions. These are called higher-order functions. They help you write cleaner, faster, and more maintainable code.
Think of functools like a toolshed for functions. Just like you have specialized tools for different jobs, functools has specialized tools for working with functions in different ways.
💡 Key concept: Functools provides functions that take other functions as arguments or return functions, making your code more flexible and powerful.
lru_cache - Cache Results
Speed Up Your Functions with Caching
lru_cache (Least Recently Used cache) is a decorator that caches the results of a function. If you call the function with the same arguments, it returns the cached result instead of recomputing.
# lru_cache - Speed Up Functions
from functools import lru_cache
import time
print("=" * 50)
print("LRU_CACHE - SPEED UP FUNCTIONS")
print("=" * 50)
# ============================================================
# WITHOUT CACHE - Slow
# ============================================================
print("\n1. WITHOUT CACHE")
def fibonacci_slow(n):
if n <= 1:
return n
return fibonacci_slow(n-1) + fibonacci_slow(n-2)
print(" Computing fibonacci(35) without cache...")
start = time.time()
result = fibonacci_slow(35)
print(f" Result: {result}")
print(f" Time: {time.time() - start:.4f}s")
# ============================================================
# WITH LRU_CACHE - Fast
# ============================================================
print("\n2. WITH LRU_CACHE")
@lru_cache(maxsize=100)
def fibonacci_fast(n):
if n <= 1:
return n
return fibonacci_fast(n-1) + fibonacci_fast(n-2)
print(" Computing fibonacci(35) with cache...")
start = time.time()
result = fibonacci_fast(35)
print(f" Result: {result}")
print(f" Time: {time.time() - start:.4f}s")
print(" Cache info:")
print(f" Hits: {fibonacci_fast.cache_info().hits}")
print(f" Misses: {fibonacci_fast.cache_info().misses}")
print(f" Maxsize: {fibonacci_fast.cache_info().maxsize}")
# Clear cache
fibonacci_fast.cache_clear()
print(" Cache cleared!")
# ============================================================
# CACHE WITH EXPENSIVE OPERATIONS
# ============================================================
print("\n3. CACHE WITH EXPENSIVE OPERATIONS")
@lru_cache(maxsize=10)
def expensive_operation(x):
"""Simulate an expensive calculation"""
print(f" Computing for {x}...")
time.sleep(0.5) # Simulate heavy work
return x * x
print(" First call - computes:")
print(f" expensive_operation(5) = {expensive_operation(5)}")
print(" Second call - uses cache:")
print(f" expensive_operation(5) = {expensive_operation(5)}")
print(" Different argument - computes:")
print(f" expensive_operation(7) = {expensive_operation(7)}")
print(" Cache info:")
print(f" {expensive_operation.cache_info()}")
# ============================================================
# CACHE WITH MAXSIZE
# ============================================================
print("\n4. CACHE WITH MAXSIZE")
@lru_cache(maxsize=3)
def add(a, b):
print(f" Computing {a} + {b}...")
return a + b
print(" Adding with cache limit 3:")
print(f" add(1, 2) = {add(1, 2)}")
print(f" add(3, 4) = {add(3, 4)}")
print(f" add(5, 6) = {add(5, 6)}")
print(f" add(7, 8) = {add(7, 8)} # Oldest removed")
print(f" Cache info: {add.cache_info()}")
# ============================================================
# WHEN TO USE LRU_CACHE
# ============================================================
print("\n5. WHEN TO USE LRU_CACHE")
print("""
Use lru_cache when:
- Function is expensive to compute
- Function is called many times with same arguments
- Function is pure (no side effects)
- You want to speed up your code
Don't use lru_cache when:
- Function has side effects
- Arguments include mutable objects
- Memory is very limited
- Function returns different results for same arguments
""")
lru_cache key points:
- Caches results — speeds up repeated calls
- maxsize — limits how many results to store
- cache_info() — shows hits, misses, size
- cache_clear() — clears the cache
- Pure functions — best for functions without side effects
Quick Check: What does lru_cache do? (Answer: It caches function results to speed up repeated calls with the same arguments)
partial - Fix Arguments
Create New Functions with Pre-filled Arguments
partial lets you create a new function with some arguments already fixed. It's like creating a shortcut for a function with certain default values.
# partial - Fix Arguments
from functools import partial
print("=" * 50)
print("PARTIAL - FIX ARGUMENTS")
print("=" * 50)
# ============================================================
# BASIC PARTIAL
# ============================================================
print("\n1. BASIC PARTIAL")
def multiply(a, b):
return a * b
# Create a new function that always multiplies by 2
double = partial(multiply, 2)
print(f" double(5) = {double(5)}")
print(f" double(10) = {double(10)}")
# Create a new function that always multiplies by 3
triple = partial(multiply, 3)
print(f" triple(5) = {triple(5)}")
print(f" triple(10) = {triple(10)}")
# ============================================================
# PARTIAL WITH MULTIPLE ARGUMENTS
# ============================================================
print("\n2. PARTIAL WITH MULTIPLE ARGUMENTS")
def power(base, exponent):
return base ** exponent
# Create a square function
square = partial(power, exponent=2)
print(f" square(5) = {square(5)}")
print(f" square(10) = {square(10)}")
# Create a cube function
cube = partial(power, exponent=3)
print(f" cube(5) = {cube(5)}")
print(f" cube(10) = {cube(10)}")
# ============================================================
# PARTIAL WITH DEFAULT VALUES
# ============================================================
print("\n3. PARTIAL WITH DEFAULT VALUES")
def greet(name, greeting="Hello", punctuation="!"):
return f"{greeting}, {name}{punctuation}"
# Create a casual greeting
casual = partial(greet, greeting="Hey", punctuation="")
print(f" casual('Alice') = {casual('Alice')}")
# Create a formal greeting
formal = partial(greet, greeting="Good day", punctuation=".")
print(f" formal('Bob') = {formal('Bob')}")
# ============================================================
# PARTIAL WITH POSITIONAL ARGUMENTS
# ============================================================
print("\n4. PARTIAL WITH POSITIONAL ARGUMENTS")
def add(a, b, c):
return a + b + c
# Fix the first argument
add_ten = partial(add, 10)
print(f" add_ten(5, 3) = {add_ten(5, 3)}")
# Fix the first two arguments
add_ten_twenty = partial(add, 10, 20)
print(f" add_ten_twenty(5) = {add_ten_twenty(5)}")
# ============================================================
# PARTIAL IN REAL CODE
# ============================================================
print("\n5. PARTIAL IN REAL CODE")
# Without partial - repetitive code
def log_info(message):
print(f"[INFO] {message}")
def log_error(message):
print(f"[ERROR] {message}")
def log_debug(message):
print(f"[DEBUG] {message}")
log_info("Application started")
log_error("File not found")
log_debug("Variable x = 10")
# With partial - less repetition
def log(level, message):
print(f"[{level}] {message}")
log_info_partial = partial(log, "INFO")
log_error_partial = partial(log, "ERROR")
log_debug_partial = partial(log, "DEBUG")
log_info_partial("Application started")
log_error_partial("File not found")
log_debug_partial("Variable x = 10")
# ============================================================
# PARTIAL WITH KEYWORD ARGUMENTS
# ============================================================
print("\n6. PARTIAL WITH KEYWORD ARGUMENTS")
def format_text(text, uppercase=False, prefix="", suffix=""):
result = text
if uppercase:
result = result.upper()
return prefix + result + suffix
# Create different formatters
uppercase = partial(format_text, uppercase=True)
prefix_star = partial(format_text, prefix="*", suffix="*")
shout = partial(format_text, uppercase=True, prefix="!", suffix="!")
print(f" uppercase('hello') = {uppercase('hello')}")
print(f" prefix_star('hello') = {prefix_star('hello')}")
print(f" shout('hello') = {shout('hello')}")
partial key points:
- Fixes arguments — creates a new function with some arguments pre-filled
- Positional or keyword — works with both types of arguments
- Reduces repetition — avoids writing the same code over and over
- Clearer code — makes your intent obvious
Quick Check: What does partial do? (Answer: It creates a new function with some arguments pre-filled)
reduce - Reduce Sequences
Reduce a Sequence to a Single Value
reduce applies a function cumulatively to all items in a sequence, reducing it to a single value.
# reduce - Reduce Sequences
from functools import reduce
print("=" * 50)
print("REDUCE - REDUCE SEQUENCES")
print("=" * 50)
# ============================================================
# BASIC REDUCE
# ============================================================
print("\n1. BASIC REDUCE")
# Sum all numbers
numbers = [1, 2, 3, 4, 5]
total = reduce(lambda a, b: a + b, numbers)
print(f" Sum of {numbers} = {total}")
# Product of all numbers
product = reduce(lambda a, b: a * b, numbers)
print(f" Product of {numbers} = {product}")
# ============================================================
# REDUCE WITH INITIAL VALUE
# ============================================================
print("\n2. REDUCE WITH INITIAL VALUE")
# Sum with initial value
total = reduce(lambda a, b: a + b, numbers, 10)
print(f" Sum of {numbers} + 10 = {total}")
# Product with initial value
product = reduce(lambda a, b: a * b, numbers, 2)
print(f" Product of {numbers} * 2 = {product}")
# ============================================================
# REDUCE WITH STRINGS
# ============================================================
print("\n3. REDUCE WITH STRINGS")
words = ["Hello", " ", "World", "!"]
sentence = reduce(lambda a, b: a + b, words)
print(f" Joining {words} = '{sentence}'")
# Longest word
words = ["apple", "banana", "cherry", "date", "elderberry"]
longest = reduce(lambda a, b: a if len(a) > len(b) else b, words)
print(f" Longest word in {words} = '{longest}'")
# ============================================================
# REDUCE WITH COMPLEX OPERATIONS
# ============================================================
print("\n4. REDUCE WITH COMPLEX OPERATIONS")
# Find the maximum
numbers = [3, 7, 2, 9, 5, 1]
max_num = reduce(lambda a, b: a if a > b else b, numbers)
print(f" Max of {numbers} = {max_num}")
# Find the minimum
min_num = reduce(lambda a, b: a if a < b else b, numbers)
print(f" Min of {numbers} = {min_num}")
# Count occurrences
data = [1, 2, 3, 2, 1, 4, 2, 3]
counts = reduce(lambda acc, x: {**acc, x: acc.get(x, 0) + 1}, data, {})
print(f" Counts of {data} = {counts}")
# ============================================================
# REDUCE VS BUILT-IN FUNCTIONS
# ============================================================
print("\n5. REDUCE VS BUILT-IN FUNCTIONS")
numbers = [1, 2, 3, 4, 5]
# Using built-in functions (clearer)
builtin_sum = sum(numbers)
builtin_prod = 1
for n in numbers:
builtin_prod *= n
# Using reduce (more flexible)
reduce_sum = reduce(lambda a, b: a + b, numbers)
reduce_prod = reduce(lambda a, b: a * b, numbers)
print(f" Sum: built-in = {builtin_sum}, reduce = {reduce_sum}")
print(f" Product: built-in = {builtin_prod}, reduce = {reduce_prod}")
print("\n When to use reduce:")
print(" - When no built-in function exists")
print(" - For complex accumulations")
print(" - For combining multiple operations")
reduce key points:
- Cumulative operation — applies function to all items
- Initial value — optional starting value
- Flexible — works with any function
- Use when no built-in — if sum, max exist, use them
Quick Check: What does reduce do? (Answer: It applies a function cumulatively to all items in a sequence, reducing it to a single value)
wraps - Preserve Metadata
Keep Your Function's Identity
wraps is a decorator for decorators. It preserves the metadata (like name, docstring) of the original function when you decorate it.
# wraps - Preserve Metadata
from functools import wraps
import functools
print("=" * 50)
print("WRAPS - PRESERVE METADATA")
print("=" * 50)
# ============================================================
# WITHOUT WRAPS - Lost Metadata
# ============================================================
print("\n1. WITHOUT WRAPS")
def simple_decorator(func):
def wrapper(*args, **kwargs):
print(" Before function")
result = func(*args, **kwargs)
print(" After function")
return result
return wrapper
@simple_decorator
def hello(name):
"""Say hello to someone"""
return f"Hello, {name}!"
print(f" hello('Alice') = {hello('Alice')}")
print(f" Function name: {hello.__name__}") # Shows 'wrapper', not 'hello'
print(f" Function docstring: {hello.__doc__}") # Shows None
# ============================================================
# WITH WRAPS - Preserved Metadata
# ============================================================
print("\n2. WITH WRAPS")
def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(" Before function")
result = func(*args, **kwargs)
print(" After function")
return result
return wrapper
@good_decorator
def greet(name):
"""Greet someone nicely"""
return f"Greetings, {name}!"
print(f" greet('Alice') = {greet('Alice')}")
print(f" Function name: {greet.__name__}") # Shows 'greet'
print(f" Function docstring: {greet.__doc__}") # Shows the docstring
# ============================================================
# WHAT WRAPS PRESERVES
# ============================================================
print("\n3. WHAT WRAPS PRESERVES")
def log_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
@log_decorator
def example(a, b, c=3):
"""Example function with many attributes"""
return a + b + c
print(f" Name: {example.__name__}")
print(f" Docstring: {example.__doc__}")
print(f" Module: {example.__module__}")
print(f" Signature: {functools.signature(example)}")
# ============================================================
# WRAPS IN DECORATOR CHAINS
# ============================================================
print("\n4. WRAPS IN DECORATOR CHAINS")
def decorator1(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def decorator2(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator1
@decorator2
def chained():
"""Chained function with wraps"""
return "Chained result"
print(f" Name: {chained.__name__}")
print(f" Docstring: {chained.__doc__}")
# ============================================================
# WHY WRAPS MATTERS
# ============================================================
print("\n5. WHY WRAPS MATTERS")
print("""
Without wraps:
- Function name changes to 'wrapper'
- Docstring is lost
- __module__ and __annotations__ are lost
- Debugging is harder
- IDEs can't show help
With wraps:
- Original name is preserved
- Original docstring is preserved
- All metadata is preserved
- Debugging is easier
- IDEs work properly
Always use @wraps when creating decorators!
""")
wraps key points:
- Preserves metadata — name, docstring, signature
- Essential for decorators — always use it
- Better debugging — function names stay correct
- IDE support — autocomplete and help work
Quick Check: Why should you use @wraps in your decorators? (Answer: To preserve the original function's name, docstring, and other metadata)
Real-World Example
Building a Data Processing Pipeline
# Real-World Example: Data Processing Pipeline
from functools import lru_cache, partial, reduce, wraps
import time
from datetime import datetime
print("=" * 60)
print("DATA PROCESSING PIPELINE")
print("=" * 60)
# ============================================================
# 1. LRU_CACHE - Caching Expensive Operations
# ============================================================
@lru_cache(maxsize=100)
def fetch_user_data(user_id):
"""Simulate fetching user data from database"""
print(f" FETCHING data for user {user_id} from database...")
time.sleep(0.3) # Simulate database query
return {
"id": user_id,
"name": f"User_{user_id}",
"email": f"user{user_id}@example.com",
"last_active": datetime.now().isoformat()
}
print("\n1. CACHING USER DATA")
print(" First call - fetches from database:")
user1 = fetch_user_data(1)
print(f" {user1}")
print(" Second call - uses cache:")
user1_cached = fetch_user_data(1)
print(f" {user1_cached}")
print(f" Cache info: {fetch_user_data.cache_info()}")
# ============================================================
# 2. PARTIAL - Pre-configured Processing
# ============================================================
def process_data(data, operation, multiplier=1):
"""Process data with an operation and multiplier"""
if operation == "double":
return data * 2 * multiplier
elif operation == "square":
return (data ** 2) * multiplier
elif operation == "half":
return (data / 2) * multiplier
else:
return data * multiplier
# Create pre-configured processors
double_data = partial(process_data, operation="double")
square_data = partial(process_data, operation="square")
half_data = partial(process_data, operation="half")
print("\n2. PRE-CONFIGURED PROCESSORS")
print(f" double_data(10) = {double_data(10)}")
print(f" square_data(5) = {square_data(5)}")
print(f" half_data(10) = {half_data(10)}")
# With multiplier
double_3x = partial(process_data, operation="double", multiplier=3)
print(f" double_3x(10) = {double_3x(10)}")
# ============================================================
# 3. REDUCE - Aggregating Data
# ============================================================
def calculate_total_revenue(sales):
"""Calculate total revenue from sales data"""
return reduce(lambda total, sale: total + sale["amount"], sales, 0)
def find_most_popular_product(sales):
"""Find the most popular product"""
product_counts = reduce(
lambda acc, sale: {**acc, sale["product"]: acc.get(sale["product"], 0) + 1},
sales,
{}
)
return reduce(lambda a, b: a if a[1] > b[1] else b, product_counts.items())
print("\n3. AGGREGATING DATA WITH REDUCE")
sales_data = [
{"product": "Laptop", "amount": 1200},
{"product": "Phone", "amount": 800},
{"product": "Laptop", "amount": 1500},
{"product": "Tablet", "amount": 500},
{"product": "Phone", "amount": 900}
]
total = calculate_total_revenue(sales_data)
print(f" Total revenue: ${total:,}")
popular, count = find_most_popular_product(sales_data)
print(f" Most popular product: {popular} ({count} sales)")
# ============================================================
# 4. WRAPS - Logging Decorator
# ============================================================
def log_execution(func):
"""Decorator that logs function execution"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
print(f" EXECUTING: {func.__name__}")
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f" COMPLETED: {func.__name__} in {elapsed:.3f}s")
return result
return wrapper
@log_execution
def process_order(order_id, items):
"""Process an order with the given items"""
time.sleep(0.1)
return f"Order {order_id} processed with {len(items)} items"
print("\n4. LOGGING WITH WRAPS")
result = process_order(123, ["item1", "item2", "item3"])
print(f" Result: {result}")
print(f" Function name: {process_order.__name__}")
print(f" Docstring: {process_order.__doc__}")
print("\n" + "=" * 60)
print("KEY TAKEAWAYS:")
print("=" * 60)
print("""
- lru_cache: Speed up repeated expensive operations
- partial: Create pre-configured functions
- reduce: Aggregate data efficiently
- wraps: Preserve function metadata in decorators
- Combined: Build powerful data processing pipelines
""")
Real-world example key points:
- lru_cache — speeds up database queries
- partial — creates pre-configured processors
- reduce — aggregates sales data
- wraps — preserves function identity in decorators
Quick Check: Which functools function would you use to speed up repeated database queries? (Answer: lru_cache)
Best Practices
Using Functools Effectively
# Best Practices for Functools
from functools import lru_cache, partial, reduce, wraps
print("=" * 60)
print("BEST PRACTICES FOR FUNCTOOLS")
print("=" * 60)
# ============================================================
# 1. USE LRU_CACHE FOR EXPENSIVE PURE FUNCTIONS
# ============================================================
print("\n1. USE LRU_CACHE FOR EXPENSIVE PURE FUNCTIONS")
# Good - pure function, expensive to compute
@lru_cache(maxsize=128)
def factorial(n):
if n <= 1:
return 1
return n * factorial(n-1)
print(f" factorial(10) = {factorial(10)}")
print(f" Cache info: {factorial.cache_info()}")
# Bad - function with side effects
# @lru_cache
# def get_random():
# return random.random() # This is wrong!
print(" Only use for pure functions without side effects")
# ============================================================
# 2. USE PARTIAL FOR CONFIGURATION
# ============================================================
print("\n2. USE PARTIAL FOR CONFIGURATION")
def send_message(message, sender="system", priority="normal"):
return f"[{priority}] {sender}: {message}"
# Good - create configured versions
system_priority = partial(send_message, sender="system", priority="high")
user_priority = partial(send_message, sender="user", priority="normal")
print(f" system_priority('Alert!') = {system_priority('Alert!')}")
print(f" user_priority('Hello') = {user_priority('Hello')}")
# ============================================================
# 3. USE REDUCE WHEN NO BUILT-IN EXISTS
# ============================================================
print("\n3. USE REDUCE WHEN NO BUILT-IN EXISTS")
# Good - custom accumulation
def flatten_list(nested):
return reduce(lambda a, b: a + b, nested, [])
nested = [[1, 2], [3, 4], [5, 6]]
flat = flatten_list(nested)
print(f" Flatten {nested} = {flat}")
# Better - use built-in when possible
numbers = [1, 2, 3, 4, 5]
print(f" sum({numbers}) = {sum(numbers)}") # Better than reduce
# ============================================================
# 4. ALWAYS USE WRAPS IN DECORATORS
# ============================================================
print("\n4. ALWAYS USE WRAPS IN DECORATORS")
# Good - with wraps
def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
# Bad - without wraps
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@good_decorator
def good_func():
"""Good function"""
pass
@bad_decorator
def bad_func():
"""Bad function"""
pass
print(f" good_func.__name__: {good_func.__name__}")
print(f" bad_func.__name__: {bad_func.__name__}")
# ============================================================
# 5. CLEAR CACHE WHEN NEEDED
# ============================================================
print("\n5. CLEAR CACHE WHEN NEEDED")
@lru_cache(maxsize=3)
def compute(x):
return x * 2
print(f" compute.cache_info(): {compute.cache_info()}")
compute.cache_clear()
print(f" After clear: {compute.cache_info()}")
# ============================================================
# 6. SUMMARY
# ============================================================
print("\n" + "=" * 60)
print("BEST PRACTICES SUMMARY")
print("=" * 60)
print("""
- Use lru_cache for expensive pure functions
- Use partial for configuration
- Use reduce when no built-in exists
- Always use wraps in decorators
- Clear cache when data changes
- Choose the right tool for the job
""")
Best practices summary:
- lru_cache — for expensive pure functions
- partial — for configuration
- reduce — when no built-in exists
- wraps — always in decorators
- Clear cache — when data changes
Quick Check: What should you always use when creating decorators? (Answer: @wraps to preserve function metadata)
Try It Yourself
Experiment with functools in the editor below.
FUNCTOOLS - PRACTICE
==================================================
1. LRU_CACHE
Computing square of 5...
expensive_square(5) = 25
expensive_square(5) = 25
Cache info: CacheInfo(hits=1, misses=1, maxsize=10, currsize=1)
2. PARTIAL
say_hi('Alice') = Hi, Alice!
say_hello('Bob') = Hello, Bob.
3. REDUCE
Sum of [10, 20, 30, 40, 50] = 150
Max of [10, 20, 30, 40, 50] = 50
4. WRAPS
Function name: sample_function
Docstring: This is a sample function
You've Got It!
You now understand the functools module in Python. You know how to use lru_cache, partial, reduce, and wraps.
Quick Quiz
Test what you've learned:
Frequently Asked Questions
What is the functools module in Python?
When should I use lru_cache?
What's the difference between partial and lambda?
Is reduce still useful in Python?
Why is @wraps important?
Can I clear the lru_cache?
Where to Go From Here
Now that you understand the functools module, check out these related topics:
Decorators
Learn more about decorators and how wraps helps.
Learn More →Collections Module
Learn about specialized container data types.
Learn More →Itertools Module
Learn about advanced iteration tools.
Learn More →