Decorators are a "syntactic sugar" for a function that takes another function as an argument.

Usually the thing returned is another function, but it technically doesn't have to be.

Functions that take functions as arguments and return functions are referred to as higher-order functions.

@some_decorator
def foo():
    ...

# is equivalent to...

def foo():
    ...

foo = some_decorator(foo)

Decorators are often used in frameworks like Flask and FastAPI to create productive high-level abstractions.

Learning to write them can seem overwhelming at first, but once you grok the concept, writing decorators will become natural.

Just be careful not to abuse the pattern. As useful as decorators are, they add a layer of indirection that can make our code more difficult to understand.

A Simple Example

Every function in python will have a __name__ attribute that is its string name.

Let's write a decorator that prints a function's name at the time of decoration.

def print_name(func):
    print(f"The function being decorated is named {func.__name__}.")

@print_name
def greet():
    return f"Hello, world."

greet()

The function being decorated is named greet.
Traceback (most recent call last):
  at block 5, line 5
TypeError: 'NoneType' object is not callable

Why did our greet function raise an Exception?

We decorated greet with our print_name decorator, but print_name doesn't return anything.

So we basically did the equivalent of writing greet = None.

Let's fix that.

def print_name(func):
    print(f"The function being decorated is named {func.__name__}.")
    return func # < we return the original function

@print_name
def greet():
    return f"Hello, world."

for _ in range(3):
    print(greet())

# notice how we'll only print the description once, at the time we decorate `greet`. not on every invocation

The function being decorated is named greet.
Hello, world.
Hello, world.
Hello, world.

Input and Output

What if we wanted to do something with the decorated function's input or output?