Syllabus Lesson 61 of 239 · Pythonic Code, Testing & Capstone
Pythonic Code, Testing & Capstone

Decorators

A decorator is a function that wraps another function to add behavior, without changing the original's body. You apply one with the @ syntax sitting on top of a def.

The shape is always the same: take a function, define an inner wrapper that does something extra and then calls the original, and return the wrapper.

def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

@shout
def greet(name):
    return f"hi {name}"

print(greet("ada"))   # HI ADA

Writing @shout above greet is just shorthand for greet = shout(greet). From then on, the name greet points at the wrapper.

The *args, **kwargs in the wrapper let it accept whatever arguments the original takes, then forward them along. That keeps the decorator generic.

functools.wraps

Wrapping replaces the function, so the original's name and docstring get hidden behind wrapper. Fix that by decorating the wrapper itself with functools.wraps(func), which copies the metadata over. Always do this on real decorators:

import functools

def double(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs) * 2
    return wrapper

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

print(add(3, 4))        # 14
print(add.__name__)     # 'add'  (preserved by wraps)
Your turn

Write a decorator double that doubles the return value of any function it wraps. Use functools.wraps so the wrapped function keeps its __name__. Then apply it with @double to a function add(a, b) that returns a + b, so add(3, 4) gives 14 and add.__name__ is still "add".

Spotted a problem in this lesson? Report it

Code · runs in your browser
Output