Skip to content

functools.wraps

When writing decorators, functools.wraps preserves the original function's metadata. Without it, decorated functions lose their name, docstring, and other attributes.

from functools import wraps

The Problem

Without wraps

def my_decorator(func):
    def wrapper(*args, **kwargs):
        """Wrapper function."""
        print("Before call")
        result = func(*args, **kwargs)
        print("After call")
        return result
    return wrapper

@my_decorator
def greet(name):
    """Return a greeting message."""
    return f"Hello, {name}!"

# Metadata is lost!
print(greet.__name__)  # 'wrapper' — Wrong!
print(greet.__doc__)   # 'Wrapper function.' — Wrong!

The decorated function looks like wrapper, not greet.


The Solution

With wraps

from functools import wraps

def my_decorator(func):
    @wraps(func)  # Preserves func's metadata
    def wrapper(*args, **kwargs):
        """Wrapper function."""
        print("Before call")
        result = func(*args, **kwargs)
        print("After call")
        return result
    return wrapper

@my_decorator
def greet(name):
    """Return a greeting message."""
    return f"Hello, {name}!"

# Metadata preserved!
print(greet.__name__)  # 'greet' — Correct!
print(greet.__doc__)   # 'Return a greeting message.' — Correct!

What wraps Preserves

wraps copies these attributes from the original function:

Attribute Description
__name__ Function name
__doc__ Docstring
__module__ Module where defined
__qualname__ Qualified name (includes class)
__annotations__ Type hints
__dict__ Function's attribute dictionary

It also sets:

Attribute Description
__wrapped__ Reference to original function

Accessing the Original Function

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@decorator
def original():
    """Original docstring."""
    pass

# Access the unwrapped function
print(original.__wrapped__)  # <function original at 0x...>
print(original.__wrapped__.__name__)  # 'original'

Decorator Templates

Basic Decorator

from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Before
        result = func(*args, **kwargs)
        # After
        return result
    return wrapper

Decorator with Arguments

from functools import wraps

def decorator_with_args(arg1, arg2):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Use arg1, arg2 here
            print(f"Args: {arg1}, {arg2}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@decorator_with_args("hello", 42)
def my_function():
    """My function docstring."""
    pass

print(my_function.__name__)  # 'my_function'

Optional Arguments Decorator

from functools import wraps

def decorator(func=None, *, option1=None, option2=None):
    """Decorator that works with or without arguments."""
    def actual_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"Options: {option1}, {option2}")
            return func(*args, **kwargs)
        return wrapper

    if func is None:
        # Called with arguments: @decorator(option1="x")
        return actual_decorator
    else:
        # Called without arguments: @decorator
        return actual_decorator(func)

# Both work:
@decorator
def func1(): pass

@decorator(option1="custom")
def func2(): pass

Practical Examples

Logging Decorator

from functools import wraps
import logging

def log_calls(func):
    """Log function calls with arguments and return values."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"Calling {func.__name__} with {args}, {kwargs}")
        result = func(*args, **kwargs)
        logging.info(f"{func.__name__} returned {result}")
        return result
    return wrapper

@log_calls
def add(a, b):
    """Add two numbers."""
    return a + b

Timing Decorator

from functools import wraps
import time

def timer(func):
    """Measure function execution time."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timer
def slow_function():
    """A slow function."""
    time.sleep(1)
    return "done"

Retry Decorator

from functools import wraps
import time

def retry(max_attempts=3, delay=1):
    """Retry function on exception."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    print(f"{func.__name__} failed (attempt {attempt + 1})")
                    time.sleep(delay)
            raise last_exception
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def unreliable_api_call():
    """Call an unreliable API."""
    pass

Validation Decorator

from functools import wraps

def validate_types(**type_hints):
    """Validate argument types."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Check keyword arguments
            for name, expected_type in type_hints.items():
                if name in kwargs:
                    value = kwargs[name]
                    if not isinstance(value, expected_type):
                        raise TypeError(
                            f"{name} must be {expected_type.__name__}, "
                            f"got {type(value).__name__}"
                        )
            return func(*args, **kwargs)
        return wrapper
    return decorator

@validate_types(name=str, age=int)
def create_user(name, age):
    """Create a user with validated types."""
    return {"name": name, "age": age}

Why Metadata Matters

Help and Documentation

# Without wraps: help shows wrapper info
help(greet)  # Shows "wrapper function" docs

# With wraps: help shows original info
help(greet)  # Shows "Return a greeting message."

Debugging and Logging

# Without wraps: confusing stack traces
# Error in wrapper at line 5...

# With wraps: clear stack traces
# Error in greet at line 20...

Introspection Tools

# Frameworks and tools rely on __name__
import inspect

# Without wraps
inspect.signature(greet)  # Shows wrapper's signature

# With wraps
inspect.signature(greet)  # Shows greet's signature

Testing

# Test frameworks use function names
def test_greet():
    assert greet.__name__ == "greet"  # Fails without wraps!

Common Mistakes

Forgetting wraps

# Bad: loses metadata
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper  # Missing @wraps(func)!

# Good: preserves metadata
def good_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Wrong Placement

# Wrong: wraps on outer function
@wraps(func)  # This doesn't work!
def decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# Right: wraps on inner function
def decorator(func):
    @wraps(func)  # Correct placement
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

Class-Based Decorators

from functools import wraps, update_wrapper

class Decorator:
    """Class-based decorator with proper metadata."""

    def __init__(self, func):
        self.func = func
        update_wrapper(self, func)  # Use update_wrapper for classes

    def __call__(self, *args, **kwargs):
        print("Before")
        result = self.func(*args, **kwargs)
        print("After")
        return result

@Decorator
def my_func():
    """My function."""
    pass

print(my_func.__name__)  # 'my_func'

Summary

Without @wraps With @wraps
__name__ = 'wrapper' __name__ = original name
__doc__ = wrapper's doc __doc__ = original doc
No __wrapped__ __wrapped__ = original func
Confusing debugging Clear stack traces
Broken introspection Tools work correctly

Key Takeaways:

  • Always use @wraps(func) when writing decorators
  • Place @wraps on the inner wrapper function
  • Use update_wrapper() for class-based decorators
  • __wrapped__ provides access to the original function
  • Proper metadata enables debugging, documentation, and testing