Python

P
PythonHub
Progress 0%

Python

  • Home
  • History of Python
  • Applications of Python
  • Introduction To Python
    • What is Python
    • Character Set
    • Tokens in Python
    • Python Execution Mode
    • Variable And Identifiers
    • Data Types in Python
    • Operators And Expressions
    • Constants in Python
    • Assignment Statement
    • Input / Output in Python
    • Simple Python Scripts
    • Namespace in Python
    • 📝 Assignments
  • Operators in Python
    • Arithmetic Operators
    • Assignment Operators
    • Shorthand Assignment Operators
    • Relational Operators
    • Logical Operators
    • Bitwise Operators
    • Special Operators
    • 📝 Assignments
  • Input Output in Python
    • Accept Input
    • Output Formatting
    • 📝 Assignments
  • Conditional Statement
    • Decision Making
    • if Statement
    • IF-ELSE STATEMENT
    • IF-ELSE LADDER
    • NESTED IF-ELSE
    • Short Hand IF-ELSE
    • 📝 Assignments
  • Loops
    • Introduction to Loops
    • While Loop
    • Nested While Loop
    • 📝 While Loop Assignments
    • For Loop
    • For Loop Examples
    • Nested For Loop
    • Nested For Loop Examples
    • Infinite While Loops
    • Infinite For Loops
    • Break, Continue and Else in Loops
    • Difference Between For and While Loop
    • For Each Loop
    • 📝 For Each Assignments
    • 📝 All Loops Assignments
  • List
    • List in Python
    • Access List Elements
    • List Functions
    • Iterate (Loop) List
    • List Comprehension
    • 📝 Assignments
  • Tuple
    • Tuple in Python
    • Access Tuple Elements
    • Tuple Functions
    • Iterate (Loop) Tuple
    • Unpack Tuple
    • Tuple Comprehension
    • 📝 Assignments
  • Set
    • Set in Python
    • Access Set Elements
    • Set Methods
    • Iterate (Loop) Set
    • Pack/Unpack Set
    • Set Comprehension
    • 📝 Assignments
  • Dictionary
    • Dictionary
    • Access Dictionary Items
    • Dictionary Methods
    • Iterate (Loop) Dictionary
    • Formatting Dictionaries
    • Nested Dictionaries
    • Dictionary Comprehension
    • 📝 Assignments
  • Diff List Tuple Set Dictionary
    • List vs Tuple
    • List vs Set
    • List vs Dictionary
    • Tuple vs Set
    • Tuple vs Dictionary
    • Dictionary vs Set
    • 📝 Assignments
  • Exception
    • Error vs Exception
    • Types of Exception
    • Exception Handling
    • User Defined Exception
    • Logging Exception
    • 📝 Assignments
  • Functions
    • Introduction to Functions
    • Modular Programming
    • Types of Functions
    • Inbuilt Functions
    • Need For User-Defined Function
    • Elements of User Defined Function
    • Function Arguments
    • Nesting of Functions
    • Recursion
    • Global Local and Non Local
    • Python Lambda Functions
    • 📝 Assignments
  • Python Module
    • Introduction to Module
    • Inbuilt Modules in Python
    • User Defined Module
    • 📝 Assignments
  • File Handling
    • Introduction to Files
    • Create File
    • Read Files
    • Write to File
    • Rename File
    • Copy File
    • Move File
    • List Files in Directory
    • Binary Files
    • Zipping and Unzipping Files
    • 📝 Assignments
  • Strings
    • Basics of Strings
    • String Special Operators
    • String Formatting Operators
    • String Methods
    • 📝 Assignments
  • Regular Expressions
  • Python OOPS
    • Basics of Object Oriented
    • What are Classes and Objects?
    • Creating Class and Object
    • OOP vs Procedural Programming
    • Difference Between Classes and Objects
    • Constructors
    • Destructor
    • Built Class Methods and Attributes
    • Class and Instance Variables
    • Inheritance in Python
    • Single Inheritance
    • Multiple Inheritance
    • Multilevel Inheritance
    • Hierarchical Inheritance
    • Hybrid Inheritance
    • Abstraction
    • Method Overriding
    • Abstract Method
    • Interfaces in Python
    • Abstract Class vs Interface
    • Public, Private and Protected
    • Overloading vs Overriding
    • Inheritance vs Composition
    • Encapsulation
    • Polymorphism
    • Inner Classes
    • 📝 Assignments
  • Advanced Python
    • Iterator in Python
    • Generator in Python
    • Decorator in Python
    • Type Hints in Python
    • Async/Await Programming
    • Dataclasses in Python
    • Context Managers in Python
    • Property Decorator in Python
    • __slots__ in Python
    • Enums in Python
    • Walrus Operator in Python
    • Match-Case in Python
    • 📝 Assignments
  • Python Standard Library
    • Collections Module
    • Itertools Module
    • Functools Module
    • Datetime Module
    • JSON Module
    • OS Module
    • Sys Module
    • Random Module
    • Math Module
    • 📝 Assignments
  • Python Testing
    • Unit Testing in Python
    • Pytest Framework
    • Mocking in Python
    • 📝 Assignments
  • Python Best Practices
    • PEP 8 Style Guide
    • Docstrings in Python
    • Logging in Python
    • Code Optimization Tips
    • Debugging Techniques
    • 📝 Assignments
  • MySQL Database in Python
    • Introduction to MySQL with Python
    • DBMS vs File System
    • Connecting to MySQL Database
    • Create Database in MySQL
    • Create Table in MySQL
    • Insert Data in MySQL
    • Insert Multiple Rows
    • Select Data from MySQL
    • WHERE Clause in MySQL
    • Update Data in MySQL
    • Delete Data from MySQL
    • Parameterized Queries
    • Transaction Management
    • Error Handling
    • Connection Pooling
    • MySQL Drivers Guide
    • Joins in MySQL
    • Aggregation Functions
    • Backup MySQL Database
    • Best Practices
    • 📝 Assignments
  • MySQL Database Operations
    • SELECT Statement
    • MySQL Operators
    • DDL Statements
    • DML Statements
    • Subqueries
    • JOIN Operations
    • Aggregation
    • Case Study
    • 📝 Assignments
  • Graphics in Python
  • Threads in Python
    • Introduction to Threads
    • Process vs Threads
    • Concurrent Programming & GIL
    • Uses of Threads
    • Creating Threads
    • Single Tasking
    • Multi Tasking
    • Thread Synchronization
    • 📝 Assignments
  • Interview Questions & Answers
  • Python Case Studies
  • Multiple Choice Questions
  • 📝 Practice Problems
Get in Touch
  • tech2dsm@gmail.com

© Sankalan Data Tech

Python Language Interactive Tutorial

Python: Functools Module

Python Functools Module - Complete Guide

Higher-order functions that make your code cleaner and faster.

Created by Sankalan Data Tech Team Verified
Data Engineers, Analysts, Scientists & Trainers
Created by experienced Python developers, data engineers, and data scientists to make programming easy through practical examples, real-world experience, and clear explanations.
On this page:
  • What is Functools?
  • lru_cache - Cache Results
  • partial - Fix Arguments
  • reduce - Reduce Sequences
  • wraps - Preserve Metadata
  • Real-World Example
  • Best Practices
  • Try It Yourself
  • Quick Quiz
  • Frequently Asked Questions
Share this tutorial:
Twitter LinkedIn Facebook WhatsApp Reddit Telegram Email Copy Link
What You'll Learn Here
  • 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

1

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

2

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

3

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

4

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

5

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

6

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.

Loading Pyodide... 0%
Python Code Editor
==================================================
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:

1. What does lru_cache do?
2. What does partial do?
3. What does reduce do?
4. Why should you use @wraps in decorators?
5. Which functools function would you use to cache expensive function results?

Frequently Asked Questions

What is the functools module in Python? ▼

The functools module provides higher-order functions that work on other functions. It includes tools like lru_cache for caching, partial for fixing arguments, reduce for reducing sequences, and wraps for preserving function metadata.

When should I use lru_cache? ▼

Use lru_cache when you have a function that is expensive to compute, is called many times with the same arguments, and has no side effects. It's great for recursive functions, database queries, and API calls.

What's the difference between partial and lambda? ▼

partial creates a new function with pre-filled arguments, preserving the original function's name and docstring. lambda creates an anonymous function that is more flexible but doesn't preserve metadata. Use partial when you want a named, specialized version of an existing function.

Is reduce still useful in Python? ▼

Yes, reduce is still useful for custom accumulations where no built-in function exists. However, for common operations like sum, max, min, the built-in functions are clearer and preferred.

Why is @wraps important? ▼

@wraps preserves the original function's name, docstring, annotations, and other metadata. This makes debugging easier, helps with documentation, and ensures that tools like IDEs and help() work correctly with decorated functions.

Can I clear the lru_cache? ▼

Yes, you can use cache_clear() to clear the cache. This is useful when the data changes and you need to recompute values. You can also check cache statistics with cache_info().

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 →
Interview Resources
  • Python Syntax & Variables Interview Questions
  • Top SQL Interview Questions & Answers
  • SQL Joins: Displaying Data from Multiple Tables FAQ
  • Python Lists and Dictionaries Interview Questions
  • Python OOP Interview Questions
  • SQL Set Operators Interview Questions
Previous: Itertools Next: Datetime →