Decorators are syntactic sugar for wrapping functions inside other functions, like this:

>>> def decorator(the_callback):
...     def wrapper():
...         print('Behavior to add')
...         the_callback()
...     return wrapper
...
>>> def func(): ...
>>> func = decorator(func)

How to build and use them

To pass in parameters and return values to and from a decorated function, we need to do this:

def decorator(function):
 
  def wrapper(*args, **kwargs):
    result = function(*args, **kwargs)
    return result
    
  return wrapper

Using *args and **kwargs makes the decorator much more dynamic because it can be then used with functions that take any number of arguments, instead of hard coding the arguments in.

To pass parameters into the decorator itself, we need a decorator creator function:

def creator(your_argument=None):
  def decorator(function):
    def wrapper(*args, **kwargs):
      return function(*args, **kwargs)
    return wrapper
  return decorator
 
@creator(your_argument=True)
def my_func():
  print('Hello world!')

To maintain/expose the name, docstrings and so on of decorated functions, one should use functools.wraps:

from functools import wraps
 
def creator(your_argument=None):
  def decorator(function):
    @wraps(function)
    def wrapper(*args, **kwargs):
      return function(*args, **kwargs)
    return wrapper
  return decorator

Use cases

To intercept calls

Decorators can be used to run checks on the function call and decide if the original code is being run or a separate function is run.

For example, functools.cache checks if there is a cached value. Django’s django.contrib.auth.decorators.login_requiredchecks if the user is authenticated and offers a redirectable login view instead if not. pydantic.validate_call checks if function input matches the types and if not, raises an error.

To register functions

Some decorators return the original function back instead of wrapping it. Instead, they keep track of the references to that function.

Flask uses this approach to register endpoints with functions:

@app.route('/')
def index():
    return 'Index Page'
 
@app.route('/hello')
def hello():
    return 'Hello, World'

Other resources

Luciano Ramalho - Decorators and descriptors decoded - PyCon 2017 - YouTube

and

Katie Silverio Decorators, unwrapped How do they work PyCon 2017 - YouTube

Katie words it really well when she says (paraphrased): “Decorators make it clear that it’s something extra that is not inherit to the original function”

Katie walks through decorator functionality step by step:

  1. Writing it directly to source mixes up utility code with the main logic
  2. Creating a wrapper function that is passed the function and function arguments that will be run is annoying to add around every function call
  3. A partial that returns a new function that does both timer wrapping and the original logic
  4. Assign that partial to the same name as the original definition = decorating a function!

A 3-part series of tutorials for decorators