You need to add logging to 50 functions, or timing to every API endpoint, or authentication checks everywhere. Instead of modifying each function, decorators let you wrap functions with reusable behavior - just add @log or @timed above any function definition.

Metadata Problem

simple.py
# Simple decorator


def shout_decorator(func):
    def wrapper():
        print("BEFORE!")
        func()
        print("AFTER!")

    return wrapper


message = 

# Decorate with @
@shout_decorator
def greet():
    print(message)


greet()

# Simple decorator


def shout_decorator(func):
    def wrapper():
        print("BEFORE!")
        func()
        print("AFTER!")

    return wrapper


message = 

# Decorate with @
@shout_decorator
def greet():
    print(message)


greet()

# Simple decorator


def shout_decorator(func):
    def wrapper():
        print("BEFORE!")
        func()
        print("AFTER!")

    return wrapper


message = 

# Decorate with @
@shout_decorator
def greet():
    print(message)


greet()

use_cases.py
# Common use-cases

import time
from functools import wraps


def log_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"calling {func.__name__}({args}, {kwargs})")
        return func(*args, **kwargs)

    return wrapper


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

    return wrapper


# Apply decorators
@log_calls
def add(a, b):
    return a + b


@timing
def slow_task():
    time.sleep(0.01)
    return "done"


print(add(2, 3))
print(slow_task())

multiple.py
# Multiple decorators


def upper(func):
    def wrapper():
        result = func()
        return result.upper()

    return wrapper


def exclaim(func):
    def wrapper():
        result = func()
        return result + "!"

    return wrapper


# Bottom to top application
@upper
@exclaim
def greet():
    return "hello"


print(greet())

# Equivalent to
def greet2():
    return "hello"


greet2 = upper(exclaim(greet2))
print(greet2())

execution_order.py
# Decorator execution order


def trace(func):
    print(f"decorating {func.__name__}")

    def wrapper():
        print(f"calling {func.__name__}")
        func()

    return wrapper


# Decorator runs at definition time
print("defining function...")


@trace
def hello():
    print("hello from function")


print("calling function...")
hello()

metadata.py
# Preserving metadata

from functools import wraps


def without_wraps(func):
    def wrapper():
        func()

    return wrapper


def with_wraps(func):
    @wraps(func)
    def wrapper():
        func()

    return wrapper


@without_wraps
def func_a():
    """doc for func_a"""
    pass


@with_wraps
def func_b():
    """doc for func_b"""
    pass


# Compare metadata
print("without wraps:")
print("  name:", func_a.__name__)
print("  doc:", func_a.__doc__)

print("\nwith wraps:")
print("  name:", func_b.__name__)
print("  doc:", func_b.__doc__)

decorator - a function that takes a function and returns a modified version, applied with @decorator syntax
decorator stacking - applying multiple decorators, executed from bottom to top

Exercise: practical.py

Create a retry decorator that attempts a function multiple times