Skip to content

Week 2 · Python for real work

Decorators

Add extra behaviour to a function without changing the code inside it, and learn to read the decorators you'll meet in real libraries.

Live session

A decorator adds extra behaviour to a function without changing the code inside it. You can spot one by the @ on the line above a function:

@log_call
def clean_customer_name(name):
    return name.strip().title()

They're often used for logging, timing and permission checks. You'll mostly use decorators that libraries give you, so the main goal of this page is to understand what that @ line does.

How to follow along

Work in your learn-python project. Create a file called decorators.py, type each example into it and run it with:

uv run decorators.py

Compare what you see with the Output under each example, and clear the file before you type the next one.

Functions are values

A function is a value, just like a number or a list. You can store it in another variable, and you can pass it to another function:

decorators.py
def format_rupees(amount):
    return f"Rs {amount:,}"


def show_total(formatter, total):
    print(formatter(total))


display = format_rupees
print(display(12800))

show_total(format_rupees, 4500)

Output:

Rs 12,800
Rs 4,500

Notice there are no brackets in display = format_rupees. Without brackets you're referring to the function itself. With brackets, as in format_rupees(12800), you're running it. Decorators depend on this idea.

A simple decorator

A decorator is a function that takes a function and returns a new one. The new function usually does something extra, then runs the original:

decorators.py
def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)

    return wrapper


@log_call
def clean_customer_name(name):
    return name.strip().title()


print(clean_customer_name("  noor foods "))

Output:

Calling clean_customer_name
Noor Foods

Line by line:

  • log_call(func) receives the original function.
  • wrapper is a new function that does the extra work, then runs the original. It's defined inside log_call, which is allowed in Python.
  • *args, **kwargs means "accept any arguments, and pass them all along". This lets the decorator work on any function, whatever its parameters. You can treat it as a pattern to copy for now.
  • func.__name__ holds the function's name as text.
  • return wrapper hands back the new function.

What the @ line really does

@log_call is a shorter way of writing this:

decorators.py
def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)

    return wrapper


def clean_customer_name(name):
    return name.strip().title()


clean_customer_name = log_call(clean_customer_name)

print(clean_customer_name("  noor foods "))

Output:

Calling clean_customer_name
Noor Foods

The name clean_customer_name now points to wrapper. So every time you call clean_customer_name(), wrapper runs first, prints its message and then runs your original function.

Keeping the function's name with wraps

Our decorator has a side effect: the function now reports the wrapper's name instead of its own.

decorators.py
def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)

    return wrapper


@log_call
def clean_customer_name(name):
    return name.strip().title()


print(clean_customer_name.__name__)

Output:

wrapper

That's confusing when you're reading logs or debugging. The wraps decorator from the standard library's functools module fixes it by copying the original function's name and description onto the wrapper:

decorators.py
from functools import wraps


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

    return wrapper


@log_call
def clean_customer_name(name):
    """Remove extra spaces and capitalise a customer's name."""
    return name.strip().title()


print(clean_customer_name.__name__)
print(clean_customer_name.__doc__)

Output:

clean_customer_name
Remove extra spaces and capitalise a customer's name.

The text in triple quotes is a docstring, a short description of what a function does, and __doc__ reads it. Whenever you write a decorator, add @wraps(func) to the wrapper. It takes one line.

Decorators you'll use

You won't write many decorators yourself, but you'll use them often in the libraries you meet later in the course:

@app.get("/orders")
def list_orders():
    ...

This is from FastAPI, which you'll use in week 3. It means "when someone asks for the /orders URL, run this function". Notice that this decorator has brackets. app.get("/orders") runs first and gives back the decorator that gets applied.

Decorators don't always wrap a function with extra steps. This one mainly records the function so that FastAPI knows when to call it. Either way, you write your function normally and add one line above it.