Python Decorators, Explained
Decorators in Python are a very powerful albeit mind-bending and under-documented tool. They are useful for many things, including changing how a function is executed (e.g. for denoting that an API endpoint requires authentication) or to register a function as a handler (e.g. to register different parser functions for different file types).
This article briefly explains how decorators work in Python and then shows different patterns of decorators that I have seen in the wild over the years, in the hope that that will make decorators slighly less confusing. Finally, it will go into some examples of how decorators can be used for different programming tasks.
What a Decorator is
Say you have a function definition for my_decorator
like the following:
def my_decorator(f):
print('function {} was passed to my_decorator'.format(f))
return f
You can then use this function as a decorator:
@my_decorator
def greet(name):
print('hello, {}'.format(name))
# this will first print : "function <function greet at 0x....> was passed to my_decorator"
# and then it will print: "hello, world"
greet('world')
All that a decorator is is simply syntactic sugar for applying the decorator function to the decorated function and making the return value the new value for the function's name:
def greet(name):
print('hello, {}'.format(name))
# this is what the decorator desugars to
greet = my_decorator(greet)
# this will first print : "function <function greet at 0x....> was passed to my_decorator"
# and then it will print: "hello, world"
greet('world')
Decorator Patterns
Now that you understand how decorators work, I want to show and explain the main patterns used for implementing decorators in the wild. While I am listing three different patterns in this article, you want to use pattern 2 in most situations since it gives the highest amount of flexibility. Pattern 1 is easier to understand and pattern "1/2" is a hacky solution for migrating from pattern 1 to pattern 2.
Pattern 1: Simple wrapping decorators
The "simple" decorator pattern takes a function f
and wraps it with an inner
function that does some magic (here: printing the arguments with which f
was called) and then calls the original f
:
import functools
###### Decorator definition
def decorator_simple(f):
@functools.wraps(f)
def inner(*args):
print('decorator_simple called on function with args: ()'.format(args)
return f(*args)
return inner
###### Functions using decorator
@decorator_simple
def fun_with_simple(name):
print("Hello {}".format(name))
###### Usage examples
# decorator_simple called on function with args: ('world',)
# Hello world
fun_with_simple('world')
The decorator functools.wraps(f)
(see docs) denotes that inner
is a wrapped version of f
and should inherit the attributes from f
such as the function name, the function's docstring and the function's type annotation.
Pattern 2: Wrapping decorators with arguments
If you want to accept arguments within a decorator, there will be triply-nested functions: an "outer" function that takes the arguments, the actual decorator function that takes the function to be decorated, and the "inner" function that will be returned from the decorator:
import functools
###### Decorator definition
def decorator_with_args(decorator_argument="ASDF"):
def the_decorator(f):
@functools.wraps(f)
def inner(*args):
print("decorator_with_args with decorator argument {} saw args: {}".format(decorator_argument, args))
return f(*args)
return inner
return the_decorator
###### Functions using decorator
@decorator_with_args()
def fun_with_decorator_args_default(name):
print("Salutations {}".format(name))
@decorator_with_args('MORE ARGS!')
def fun_with_decorator_args_configured(name):
print("Greetings {}".format(name))
###### Usage examples
# decorator_with_args with decorator argument ASDF saw args: ('earth',)
# Salutations earth
fun_with_decorator_args_default('earth')
# decorator_with_args with decorator argument MORE ARGS! saw args: ('universe',)
# Greetings universe
fun_with_decorator_args_configured('universe')
Pattern 1/2: "Double-use" decorators
One trouble with pattern 1 is that you will never be able to pass arguments to the decorator later on because @decorator(some_param)
will be interpreted as invoking the inner
function using the parameter some_param
. If you have designed your decorator to use pattern 1 but now would like to accept parameters, there is the possibility to keep backwards compatibility while still supporting optional arguments to the decorator, though the solution is hacky:
import functools
###### Decorator definition
def decorator_doubleuse(decorator_argument="GHJK"):
def the_decorator(f):
@functools.wraps(f)
def inner(*args):
print("decorator_doubleuse with decorator argument {} saw args: {}".format(decorator_argument, args))
return f(*args)
return inner
if callable(decorator_argument):
# decorator_doubleuse was called as a bare identifier, not a function
# the function-to-be-decorated was erroneously passed as decorator_argument
# fidget things around to pretend that decorator_doubleuse was called as a function
# assign the function to a properly-named variable
f = decorator_argument
# reset the argument to its default
decorator_argument = 'GHJK'
# return the decorated function
return the_decorator(f)
return the_decorator
###### Functions using decorator
@decorator_doubleuse
def fun_with_doubleuse1(name):
print("Hi {}".format(name))
@decorator_doubleuse()
def fun_with_doubleuse2(name):
print("Sup, {}".format(name))
@decorator_doubleuse('CONFIGURABLE!')
def fun_with_doubleuse3(name):
print("Top of the morning {}".format(name))
###### Usage examples
# decorator_simple saw args: ('world',)
# Hello world
fun_with_simple('world')
# decorator_with_args with decorator argument ASDF saw args: ('earth',)
# Salutations earth
fun_with_decorator_args_default('earth')
# decorator_with_args with decorator argument MORE ARGS! saw args: ('universe',)
# Greetings universe
fun_with_decorator_args_configured('universe')
# decorator_doubleuse with decorator argument GHJK saw args: ('terra',)
# Hi terra
fun_with_doubleuse1('terra')
# decorator_doubleuse with decorator argument GHJK saw args: ('home',)
# Sup, home
fun_with_doubleuse2('home')
# decorator_doubleuse with decorator argument CONFIGURABLE! saw args: ('galaxy',)
# Top of the morning galaxy
fun_with_doubleuse3('galaxy')
Uses for Decorators
Now that we have seen some structures of how decorators are typically implemented, let's go into some use cases where I've found them useful in practice. You will notice that I am using "Pattern 2" for most cases since it allows me to extend the decorator at a later time.
Apps/APIs that require authentication
Say you have a Flask application or an API where you want to selectively control authentication and authorization to. A nice way of handling this is to write a decorator that can be added to authorization-requiring routes, only calling the route handler when authorization was successful and returning an error otherwise:
import functools
from flask import Flask, request
from werkzeug.exceptions import Unauthorized, Forbidden
app = Flask(__name__)
def requires_auth(valid_tokens={}):
def decorator(f):
@functools.wraps(f)
def inner(*args, **kwargs):
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
raise Unauthorized('No bearer token provided')
if token not in valid_tokens:
raise Forbidden('Token is invalid')
# everything OK, forward to route handler
return f(*args, **kwargs)
@app.route('/secret-society')
@requires_auth(valid_tokens={'secret', 'shibboleth'})
def greet_secret():
name = request.args.get('name', 'World')
return f'Hello {escape(name)}!'
@app.route('/hacker-society')
@requires_auth(valid_tokens={'42', 'shibboleet'})
def greet_hacker():
name = request.args.get('name', 'World')
return f'How\'s hacking, {escape(name)}?'
A small remark on the order of decorator application: If multiple decorators are given to a function, they will be applied in an inside-out order. For example, the decorators for greet_secret
desugar to:
greet_secret = requires_auth(valid_tokens={'secret', 'shibboleth'})(greet_secret)
greet_secret = app.route('/secret-society')(greet_secret)
Registering a function handler
Say you want to write a tool that can take different types of nested document formats such as JSON, YAML and TOML, query them and print their structure in a human-readable way akin to something like jq (an awesome tool that I highly recommend). A good way of registering different parser functions in a unified interface can be to use decorator in combination with a function to use for dispatching to the concrete implementation:
You can now use the newly-defined parser
decorator to register handler methods for different file formats:
@parser('json')
def parse_json(f):
import json
return json.load(f)
@parser('yaml')
def parse_yaml(f):
import yaml
return yaml.load(f, Loader=yaml.SafeLoader)
@parser('toml')
def parse_toml(f):
import toml
return toml.load(f)
With all this in place, you can use the parse
dispatch function to automatically select the right handler depending on the file extension:
print(parse('data/test.json'))
print(parse('data/test.yaml'))
print(parse('data/test.toml'))
Conclusion
Decorators are a powerful meta-programming tool that can often times greatly clean up repetitive code and help you structure your applications better. With that being said, don't be too smart with the possibilities that this tool opens to you. I want to end this article with the famous quote:
Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?
- Brian W. Kernighan,
"The Elements of Programming Style", 2nd edition, chapter 2