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.

A decorator is a function that wraps another function (or class) to add behavior without modifying the original code.

Simple Decorator

message
simple.py
Replay: real traced execution (multi-file project)
# Simple decorator


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

    return wrapper


message = "hello"

# 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 = "hi"

# 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 = "welcome"

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


greet()

  1. message ← hello

    13message→ hello = "hello"14#@message="hi", "welcome"
  2. def shout_decorator(func):

    4def shout_decorator(func⟨function greet A⟩):5    def wrapper():6        print("BEFORE!")7        func()8        print("AFTER!")910    return wrapper<function shout_decorator.<locals>.wrapper at ⟨addr B⟩>
  3. greet()

    22greet()
  4. def wrapper():

    4def shout_decorator(func):5    def wrapper():6        print("BEFORE!")7        func()8        print("AFTER!")
    outputBEFORE!
  5. def greet():

    6        print("BEFORE!")7        func()8        print("AFTER!")910    return wrapper111213message = "hello"14#@message="hi", "welcome"1516# Decorate with @17@shout_decorator18def greet():19    print(messagehello)
    outputhello
    AFTER!
  6. greet()

    22greet()
  1. message ← hi

    13message→ hi = "hi"
  2. def shout_decorator(func):

    4def shout_decorator(func⟨function greet A⟩):5    def wrapper():6        print("BEFORE!")7        func()8        print("AFTER!")910    return wrapper<function shout_decorator.<locals>.wrapper at ⟨addr B⟩>
  3. greet()

    21greet()
  4. def wrapper():

    4def shout_decorator(func):5    def wrapper():6        print("BEFORE!")7        func()8        print("AFTER!")
    outputBEFORE!
  5. def greet():

    6        print("BEFORE!")7        func()8        print("AFTER!")910    return wrapper111213message = "hi"1415# Decorate with @16@shout_decorator17def greet():18    print(messagehi)
    outputhi
    AFTER!
  6. greet()

    21greet()
  1. message ← welcome

    13message→ welcome = "welcome"
  2. def shout_decorator(func):

    4def shout_decorator(func⟨function greet A⟩):5    def wrapper():6        print("BEFORE!")7        func()8        print("AFTER!")910    return wrapper<function shout_decorator.<locals>.wrapper at ⟨addr B⟩>
  3. greet()

    21greet()
  4. def wrapper():

    4def shout_decorator(func):5    def wrapper():6        print("BEFORE!")7        func()8        print("AFTER!")
    outputBEFORE!
  5. def greet():

    6        print("BEFORE!")7        func()8        print("AFTER!")910    return wrapper111213message = "welcome"1415# Decorate with @16@shout_decorator17def greet():18    print(messagewelcome)
    outputwelcome
    AFTER!
  6. greet()

    21greet()
decorator - a function that takes a function and returns a modified version, applied with @decorator syntax

Decorator Equivalence

The @decorator syntax is equivalent to:

def func():
    ...
func = decorator(func)

Common Use Cases

use_cases.py
Replay: real traced execution (multi-file project)
# 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 = 1000.0
        result = func(*args, **kwargs)
        elapsed = 1000.01 - 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())

  1. def log_calls(func):

    7def log_calls(func⟨function add A⟩):8    @wraps(func)9    def wrapper(*args, **kwargs):10        print(f"calling {func.__name__}({args}, {kwargs})")11        return func(*args, **kwargs)1213    return wrapper⟨function add B⟩
  2. def timing(func):

    16def timing(func⟨function slow_task C⟩):17    @wraps(func)18    def wrapper(*args, **kwargs):19        start = 1000.020        result = func(*args, **kwargs)21        elapsed = 1000.01 - start22        print(f"{func.__name__} took {elapsed:.4f}s")23        return result2425    return wrapper⟨function slow_task D⟩
  3. print(add(2, 3))

    40print(add(2, 3))41print(slow_task())
  4. def wrapper(*args, **kwargs):

    8@wraps(func)9def wrapper(*args(2, 3), **kwargs):10    print(f"calling {func.__name__add}({args(2, 3)}, {kwargs{}})")11    return func(*args(2, 3), **kwargs{})
    outputcalling add((2, 3), {})
  5. def add(a, b):

    29@log_calls30def add(a2, b3):31    return a2 + b3
  6. print(add(2, 3))

    40print(add(2, 3))41print(slow_task())
    output5
  7. start ← 1000.0

    17@wraps(func)18def wrapper(*args(), **kwargs):19    start→ 1000.0 = 1000.020    result = func(*args(), **kwargs{})21    elapsed = 1000.01 - start
  8. def slow_task():

    34@timing35def slow_task():36    time<module 'time' (built-in)>.sleep(0.01)37    return "done"
  9. result ← done, elapsed ← 0.009999999999990905

    19start = 1000.020result→ done = func(*args(), **kwargs{})21elapsed→ 0.009999999999990905 = 1000.01 - start1000.022print(f"{func.__name__slow_task} took {elapsed0.009999999999990905:.4f}s")23return resultdone
    outputslow_task took 0.0100s
  10. print(slow_task())

    40print(add(2, 3))41print(slow_task())
    outputdone
  • Logging: print when a function is called
  • Timing: measure execution time
  • Caching: memoize results
  • Authentication: check permissions before running
  • Validation: ensure inputs are correct

Multiple Decorators

multiple.py
Replay: real traced execution (multi-file project)
# 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())

  1. def exclaim(func):

    pass 1 of 2
    12def exclaim(func⟨function greet A⟩):13    def wrapper():14        result = func()15        return result + "!"1617    return wrapper<function exclaim.<locals>.wrapper at ⟨addr B⟩>
  2. def upper(func):

    pass 1 of 2
    4def upper(func<function exclaim.<locals>.wrapper at ⟨addr B⟩>):5    def wrapper():6        result = func()7        return result.upper()89    return wrapper<function upper.<locals>.wrapper at ⟨addr C⟩>
  3. print(greet())

    27print(greet())
  4. result ← hello

    13def wrapper():14    result→ hello = func()15    return resulthello + "!"
  5. result ← hello!

    5def wrapper():6    result→ hello! = func()7    return resulthello!.upper()
  6. greet2 = upper(exclaim(greet2))

    27print(greet())2829# Equivalent to30def greet2():31    return "hello"323334greet2 = upper(exclaim(greet2⟨function greet2 D⟩))35print(greet2())
    outputHELLO!
  7. def exclaim(func):

    pass 2 of 2
    12def exclaim(func⟨function greet2 D⟩):13    def wrapper():14        result = func()15        return result + "!"1617    return wrapper<function exclaim.<locals>.wrapper at ⟨addr E⟩>
  8. def upper(func):

    pass 2 of 2
    4def upper(func<function exclaim.<locals>.wrapper at ⟨addr E⟩>):5    def wrapper():6        result = func()7        return result.upper()89    return wrapper<function upper.<locals>.wrapper at ⟨addr F⟩>
  9. greet2 ← <function upper.<locals>.wrapper at ⟨addr F⟩>

    34greet2→ <function upper.<locals>.wrapper at ⟨addr F⟩> = upper(exclaim(greet2))35print(greet2())
  10. result ← hello

    13def wrapper():14    result→ hello = func()15    return resulthello + "!"
  11. result ← hello!

    5def wrapper():6    result→ hello! = func()7    return resulthello!.upper()
  12. print(greet2())

    34greet2 = upper(exclaim(greet2))35print(greet2())
    outputHELLO!
decorator stacking - applying multiple decorators, executed from bottom to top

Execution Order

Applied from bottom to top: decorator1(decorator2(func)).

execution_order.py
Replay: real traced execution (multi-file project)
# 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()

  1. print("defining function...")

    14# Decorator runs at definition time15print("defining function...")
    outputdefining function...
  2. def trace(func):

    4def trace(func⟨function hello A⟩):5    print(f"decorating {func.__name__hello}")67    def wrapper():8        print(f"calling {func.__name__}")9        func()1011    return wrapper<function trace.<locals>.wrapper at ⟨addr B⟩>
    outputdecorating hello
  3. print("calling function...")

    23print("calling function...")24hello()
    outputcalling function...
  4. def wrapper():

    7def wrapper():8    print(f"calling {func.__name__hello}")9    func()
    outputcalling hello
  5. def hello():

    8        print(f"calling {func.__name__}")9        func()1011    return wrapper121314# Decorator runs at definition time15print("defining function...")161718@trace19def hello():20    print("hello from function")
    outputhello from function
  6. hello()

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

Metadata Problem

metadata.py
Replay: real traced execution (multi-file project)
# 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__)

  1. def without_wraps(func):

    6def without_wraps(func⟨function func_a A⟩):7    def wrapper():8        func()910    return wrapper<function without_wraps.<locals>.wrapper at ⟨addr B⟩>
  2. def with_wraps(func):

    13def with_wraps(func⟨function func_b C⟩):14    @wraps(func)15    def wrapper():16        func()1718    return wrapper⟨function func_b D⟩
  3. print(" name:", func_a.__name__)

    33# Compare metadata34print("without wraps:")35print("  name:", func_a.__name__wrapper)36print("  doc:", func_a.__doc__None)3738print("\nwith wraps:")39print("  name:", func_b.__name__func_b)40print("  doc:", func_b.__doc__None)
    outputwithout wraps:
      name: wrapper
      doc: None
    
    with wraps:
      name: func_b
      doc: None

Exercise: practical.py

Create a retry decorator that attempts a function multiple times