Decorators in Python

Table of Contents

Meet a decorator:

def decorator(fn):
    return fn

def foo():
    ...

foo = decorator(foo)

It may look differently than your ordinary @decorator, but trust me: it is the same thing. It’s a callable (like a function, or a functor), (sidenote: Functor: an object which implements a __call__ method.) which takes some entity, usually class or function, and returns a value which replaces it. Usually, when we decorate a function, decorator returns some other function, which adds some functionality. But it isn’t necessary to return the same thing. We can return something entirely different and there’s no decorator police to stop us. Madness and anarchy.

>>> def decorator(fn):
...     return "abc"
...
>>> @decorator
... def foo():
...     return 123
...
>>> print(type(foo), foo)
<class 'str'> abc

The best decorators do just one thing and then leave the decorated entity alone. That’s for the sake of maintainability and debuggability, but in theory decorator can totally replace whatever it decorates.

Writing decorators as functions is burdensome and not cool at all. They look like ordinary functions (which they are) and its becomes extra hard to make cool syntax highlighting just for them. This is why Python Gods we have introduced a syntactic sugar for boring function calls: @decorator, which we place just above a thing which we’d like to change.

@decorator
def foo():
    ...

Unfortunately, @decorator syntax breaks people’s brains. With foo = decorator(foo) you immediately know what to expect. When it comes to @decorator… this StackOverflow’s question question accumulated 3156 points since 2009 and is still growing.

My First Decorator

Let’s make one of the most typical decorators. It will log all calls to the decorated entity:

def decorator(fn):
    def wrapper():
        print("Call:", fn.__name__)
        return fn()
  return wrapper

How exactly does it work? When Python encounters a @decorator part, it takes a function named decorator, takes an entity from line below (our decorated function), and calls decorator(decorated_function). It then replaces decorated function with whatever that call returns. Presto.

Decorators aren’t lazy: python executes and evaluates decorators code immediately when it reaches a decorator. Remember: it’s only a syntactic sugar for function call and assign. This is why it’s better if decorators don’t have any heavy side effects, or their execution might gravely slow down startup time of our script or module.

Consider this decorator, which doesn’t even call the decorated foo, function, to illustrate the idea:

>>> def decorator(fn):
...   print("decorator")
...   return fn
...
>>> @decorator
... def foo():
...   return 1
...
decorator

Because of this, to do anything useful at a runtime, decorators usually return a closure – a nested callable which references data passed to decorator: our decorated function. Let’s call it with a nice generic name, like wrapper.

>>> def decorator(fn):
...     def wrapper():
...         print("Call:", fn.__name__)
...         return fn()
...     return wrapper
...
>>> @decorator
... def foo():
...     print("foo()")
...
>>> foo()
Call: foo
foo()

Now because of the decorator we’ll automatically get informed about all calls to foo().

Decorators and Arguments

So far so good, but what happens when the decorated callable accepts arguments? We can capture them and do whatever we want with them in a wrapper. Signature of wrapper should match the signature of decorated callable, or otherwise we’ll silently change its interface, and utterly confuse our future selves. A common technique is to forward all positional and keyword arguments to the decorated entity. (sidenote: An interesting fact about this technique is that it actually does change the interface of decorated entity, but because we pass everything to the original function (stored in a closure), Python will still produce the same results and errors.)

>>> def decorator(fn):
...     def wrapper(*a, **kw):
...         print("positional arguments:", *a)
...         print("keyword arguments:", kw)
...         return fn(*a, **kw)
...     return wrapper
...
>>> @decorator
... def foo(i, j, k=None):
...     print("-----")
...
>>> foo(1, "abc")
positional arguments: 1 abc
keyword arguments: {}
-----
>>> foo(1, "abc", k="bar")
positional arguments: 1 abc
keyword arguments: {'k': 'bar'}
-----

Decorators can have arguments themselves as well. Parametrizing a decorator can be understood as a way of creating a new instance of decorator every time.

In other words, when we say this:

@decorator(print_calls=True, print_args=False)
def foo():
    pass

@decorator(print_calls=True, print_args=True)
def bar():
    pass

we mean:

  1. please create a new decorator for function foo which prints its calls, but doesn’t print its arguments;
  2. please create a new decorator for function bar which prints both its calls and its arguments.

“Please” is important. It’s always good to be nice to the computers, in case they rule the world one day.

For comparison, without decorator’s syntactic sugar they’d look like this:

foo = decorator(print_calls=True, print_args=False)(foo)
bar = decorator(print_calls=True, print_args=True)(bar)

Because we know what we mean, we can now easily implement a simple function (often called factory) which returns some other function which has a very decorator-specific interface.

def decorator(print_calls=False, print_args=False):
    def actual_decorator(fn):
        def wrapper(*a, **kw):
            if print_calls:
                print("Call:", fn.__name__)
            if print_args:
                print("positional arguments:", *a)
                print("keyword arguments:", kw)

            return fn(*a, **kw)
        return wrapper
    return actual_decorator

Don’t be scared by deeply nested functions. It’s just a usual decorator wrapped by yet another function. This is arguably the easiest method of creating a closure in Python, so we don’t have to explicitly pass print_calls and print_args to the wrapper.

Optional arguments

It’s possible to write a decorator which takes both forms: parameter-less @decorator or @decorator() and decorator with parameters @decorator(parameters). I think it’s nice touch for user-facing interfaces.

This technique isn’t something they teach at schools, but it’s really, really simple. All we have to do is to detect in decorator() function what is its first argument and act accordingly. When it’s set to the callable, then it’s a good indication that we’re using @decorator form and we should return a wrapper directly. Otherwise, we’re using one of decorator factories.

def decorator(fn=None, *, parameter=None):
    def deco(wfn):
        def wrapper(*a, **kw):
            do_something(parameter)
            return wfn(*a, **kw)
        return wrapper

    if fn is None:  # @decorator(), @decorator(parameter=...)
        return deco
    return deco(fn)  # @decorator

@functools.lru_cache employs this technique.

Using Classes as Decorators

Classes can be used as decorators as well. Arguably, they might be easier to understand as such, because they have clear distinction between creating a closure (inside the constructor) and calling it (inside a __call__ dunder method).

class decorator:
    def __init__(self, fn):
        self.fn = fn

    def __call__(self, *a, **kw):
        return self.fn(*a, **kw)


@decorator
def foo():
    ...

Decorators Stacking

Decorators can be “stacked” on each other, which is an elegant design pattern for the decorator-based composition. The below snippet:

@decorator1
@decorator2
def foo():
    ...

is equivalent to:

foo = decorator1(decorator2(foo))

The interesting fact is that decorators are bound to the name of callable from the bottom to the top (in reverse order). It totally makes sense: just imagine opening a bracket after each decorator:

# Invalid syntax, for illustrative purposes only

@decorator1(
@decorator2(
def foo():
    ...
)
)

Now, when we call foo(), we really decorator’s inner wrappers will call in their natural order: wrapper1wraper2foo. This is less obvious, but still the only natural thing which may happen.

>>> def decorator1(fn1):
...     print("Applying decorator1")
...     def wrapper1(s: str) -> str:
...         return fn1(s + "_d1")
...     return wrapper1
...
>>> def decorator2(fn2):
...     print("Applying decorator2")
...     def wrapper2(s: str) -> str:
...         return fn2(s + "_d2")
...     return wrapper2
...
>>> @decorator1
... @decorator2
... def foo(s: str):
...     return s
...
Applying decorator2
Applying decorator1
>>> foo("f")
'f_d1_d2'

It is important distinction. Remember that Python evaluates decorators as soon as code execution reaches them? If decorators have any side effects at that stage, these side effects will be applied in reverse order onto different entities.

A classical example which highlights the importance of decorators order is stacking cache and property decorators. Correct way to do it is to stack @property on top of @cache. This way property decorator doesn’t interfere with memoization implementation, which caches the arguments passed to decorated function (in this case the self reference).

class Foo:
  @property
  @cache
  def bar(self):
    ...

Decorators of Classes

Classes are callables too: you call a class to create an instance of object. It means that we can decorate classes as well. This is an opportunity to not only wrap a constructor, but also to modify the class itself. I find this technique a good, easier to understand alternative to metaclasses (discussed below).

def hellostr(cls):
    def __str__(self):
        return f"Hello, {cls.__name__}, i: {self.i}"

    cls.__str__ = __str__

    def wrapper(i: int):
        return cls(i + 100)

    return wrapper

@hellostr
class Foo:
    def __init__(self, i: int):
        self.i = i

print(Foo(1))  # Hello, Foo, i: 101

In this example we change 2 things: First, we add the __str__ method and then bind the name Foo to callable which returns the original class, but called with incremented argument

Metaclass Counterpart

Metaclasses, are sometimes refered to as class factories. They provide similar possibilities to the decorator above. By defining a custom callable returned by hellostr decorator we replicated the idea behind __call__ method of metaclass. Above example could be implemented like this:

class HelloStr(type):
  def __new__(metacls, name, bases, ns, **kw):
    ns["__str__"] = lambda self: f"Hello, {name}, i: {self.i}"
    return super().__new__(metacls, name, bases, ns, **kw)

  def __call__(cls, i: int):
    return super().__call__(i + 100)

class Bar(metaclass=HelloStr):
  def __init__(self, i: int):
    self.i = i

print(Bar(1))  # Hello, Bar, i: 101

One of the most prominent class decorators is @dataclass, which automatically generates a lot of boilerplate code which you’re likely to write anyway.

Differences to Metaclasses

Metaclasses are a core mechanism to produce classes. Compared to that, decorators are more limited, but still powerful and might be just enough to get the job done. Here are the most prominent differences between metaclasses and decorators regarding the modification of classes.

Composability

Thanks to stacking, we can create many, small decorators which apply changes one after another. On the other hand, class can have only one metaclass, which must do everything. Thus, decorators approach can be cleaner and more maintainable.

Changing Class Before It’s Created

Decorators have access to already created class and can modify it, or even return a totally different one. But metaclasses, through their __new__ method can alter it even before it is created.

Such alterations usually lie in the realm of hardcore trickery, but if you need to do something before class is even created, then you should use metaclasses.

Base Classes

Inheritance-wise, many magic methods of standard metaclass interface (__new__, __init__, __prepare__) accept bases parameter, which is a tuple of base classes for a class. Thus, metaclasses can depend their behaviour on inheritance. They can even modify this list. It is impossible for decorator-based approach, because class is already constructed at the point when decorators start their work.

Classes as Decorators

Any callable can be a decorator. This includes methods of existing objects.

class API:
    def decorator(self, fn):
      def wrapper(*a, **kw):
        return fn(*a, **kw)
      return wrapper

api = API()

@api.decorator
def foo():
    ...

This technique is wide-sperad among many Python libraries and frameworks. A very popular one is Flask web framework, which uses a global app object to configure certain aspects of HTTP endpoints. For example decorating a function with @app.route("/") registers it as a handler for network requests.

Important Built-in Decorators

@wraps

All above examples have a flaw: by rebinding names to arbitrary callables we lose their names (fn.__name__) and docstrings (fn.__doc__). We could of course manually attach them to callables returned by decorators, but fortunately there is a nifty decorator (sic!) in standard library which does thas for us. We just have to attach it to the wrapper function.

from functools import wraps

def deco(fn):
  @wraps(fn)
  def wrapper(*a, **kw):
    ...
  return wrapper

@deco
def foo():
  """Hello"""
  ...

print(foo.__name__)  # foo
print(foo.__doc__)   # Hello

@property

@property is a decorator which turns a method of class into a read-only property. (sidenote: Descriptor, really.) From that point we’ll access values returned by the function through obj.func instead of obj.func(). Typically it is used to either make a real property (which is often pseudo-private) immutable or to introduce some logic when modifying it via a setter.

class Foo:
    def __init__(self):
        self._prop = 1

    @property
    def prop(self):
        return self._prop

    @prop.setter
    def prop(self, val):
        assert val > 0
        self._prop = val

    @property
    def other_prop(self):
        db = Database()
        return db.get("other property")

Although property() is a built-in implemented in C, there’s an example of how pure Python equivalent could look like. @property creates a descriptor object, which implements special __get__ and __set__ dunder methods. Apart from that, it’s an ordinary object whose instance is bound to the prop name. That’s why we can create getters, setters and deleters separately for each property.

@staticmethod, @classmethod

@staticmethod and @classmethod decorators convert methods to static and class methods (i.e. methods, which aren’t called on initialized objects, but on classes themselves). (sidenote: Technically, class and static methods can be used on objects as well: both Foo.cmethod() and Foo().cmethod() will work the same.)

The distinction between the two is subtle and often there will be no practical difference between them. Class methods receive class as the implicit first argument and static methods do not.

class Foo:
    @classmethod
    def cmethod(cls):
        ...

    @staticmethod
    def smethod():
        ...

Class methods are often used to create alternative constructors:

class Foo:
    def __init__(self, arg1, arg2, arg3, arg4, arg5):
        ...

    @classmethod
    def from_fibonacci(cls):
        return cls(1, 1, 2, 3, 5)

@contextmanager

One of my favourite decorators is @contextmanager. It abuses yield keyword inside a generator function to turn it into a context manager used with with statements. With @contextmanager Python executes everything until yield statement, then enters the with block, then executes everything after yield. This is so much easier than writing ordinary context managers with __enter__ and __exit__ methods.

One of my favourite context managers created this way temporarily changes the current working directory. I write it for most of my programs which need to operate on a file system. One of its best properties: it can be nested.

from contextlib import contextmanager


@contextmanager
def chdir(path):
  cwd = os.getcwd()
  try:
    os.chdir(path)
    yield
  finally:
    os.chdir(cwd)


with chdir(".."):
  do_sth()
  with chdir("/var/log"):
    do_sth_else()

@cache

@cache decorator is useful for computionally-heavy and frequently called functions. It remembers the arguments passed to the function and its return value. Next time the function is called with cached set of arguments, @cache will return stored return value instead of running the function once again.

@cache is more modern version of @lru_cache. The latter, however, allows choosing how many values should be stored, which should prevent unbounded memory usage.

from functools import cache

@cache
def foo(arg1, arg2):
  ret = heavy_computations(arg1, arg2)
  return ret

foo(1, 2)  # 20 seconds
foo(1, 2)  # 0 seconds
foo(1, 3)  # 20 seconds
foo(1, 3)  # 0 seconds

Ideas for Decorators

Argparse Wrapper

I’ve been using decorators to implement a wrapper for argparse library to simplify creating applications with subcommands.

The source code of the snippet below is provided under the terms of GPLv3, contrary to the general license of the snippets presented on this page.
# SPDX-License-Identifier: GPL-3.0-only
# Copyright (C) 2022 Michał Góral

import argparse
import textwrap


class ArgumentParser:
    def __init__(self, *a, **kw):
        self.parser = argparse.ArgumentParser(*a, **kw)
        self._subparsers = None
        self._subpmap = {}

    def parse_args(self, *a, **kw):
        return self.parser.parse_args(*a, **kw)

    def subcommand(self, name: str = None):
        def _deco(fn):
            if not self._subparsers:
                self._subparsers = self.parser.add_subparsers()

            subpname = name if name else fn.__name__
            parser = self._subparsers.add_parser(
                subpname,
                description=textwrap.dedent(fn.__doc__),
                formatter_class=argparse.RawDescriptionHelpFormatter,
            )
            parser.set_defaults(func=fn)
            self._subpmap[fn] = parser
            return fn

        return _deco

    def arg(self, *a, **kw):
        def _deco(fn):
            subp = self._subpmap[fn]
            subp.add_argument(*a, **kw)
            return fn

        return _deco


# Usage:
cli = args.ArgumentParser()

@cli.arg("foo", default=42, help="some help")
@cli.arg("bar", help="some help")
@cli.subcommand()
def mycommand():
    pass

It is very important to remember about the order in which decorators are applied and called (see: Decorators Stacking): from the bottom to the top. In this case it means that decorator which creates a subcommand should be just above the decorated function and the order of subcommand’s arguments is reversed as well.

Repeating Unsuccessful Calls

This technique is useful for non-CPU bound operations which program can’t control, like network calls or I/O. Decorator simply repeats the last function when it fails. Just for fun I implemented it as a exponential backoff, with rate limiting in mind.

from time import sleep

def repeat(fn):
    t = 1
    stop = 32
    def wrapper(*a, **kw):
        while True:
          ret = fn(*a, **kw)
          if ret:
            return ret
          sleep(t)
          if t < stop:
            t *= 2
        return ret
    return wrapper


@repeat
def foo():
    assert False

Function Registry

This is a big but simple idea. We implement a central (usually global) registry of functions which we can use any way we want. We may implement a plugin system of automatic dispatching of filters and vlidators for registered functions.

Keep in mind that you see only a skeleton. The true fun begins with how you use it.

REGISTRY = []

def register(fn):
    REGISTRY.append(fn)
    return fn

@register
def foo():
    ...

@register
def bar():
    ...

Annotating Callables

Pytest uses this technique to implement test marks: labels attached to test functions which allow users to filter tests which Pytest runs.

In below example we select all registered functions (see above) which are really important to us.

def mark(name: str)
    def deco(fn):
        setattr(fn, name, True)
        return fn
    return deco

@register
@mark("important")
def foo():
    ...

importants = [f for f in REGISTRY if getattr(f, "important", False)]