Intermediate

Python 3 Decorators

This course covers Python decorators: what they are and how to use them to decorate functions.

Table of Contents

  1. Course Overview
  2. Working with Higher Order Functions and Closures
  1. Implement function decorators
  1. Use advanced decorator workflows
  1. Decorating classes and Class Decorators

1. Course Overview

This course covers Python decorators: what they are and how to use them to decorate functions.

The course covers in order:

  1. higher-order functions, closures and non-local variables, in order to understand how decorator functions are implemented in practice.
  2. Implementing custom decorators.
  3. Decorators with arguments and how to apply multiple decorators on a single function.
  4. Using classes as decorators and how to decorate classes (instead of functions).

Major topics covered

  • Implementing decorators
  • Preserving metadata of a function (functools.wraps)
  • Decorators with arguments
  • Application of several decorators (stacking)
  • Class decorators

Final objective

By the end of this course, you will know what decorators are, how they work, and how to create your own decorators to extend the functionality of a function.

Prerequisites

Be familiar with the fundamentals of the Python language, including functions. Experience in object-oriented programming (OOP) is an asset.


2. Working with Higher Order Functions and Closures


2.1 Introduction and prerequisites

This course was created with a specific version of Python. The information applies to the versions of Python shown in the course.

This is an advanced level course. You should already have intermediate level knowledge of Python. If you are a beginner, it is recommended that you review other introductory Pluralsight courses before returning to this one.

Essential prerequisites:

  • Mastery of Python functions (this is the fundamental building block for understanding decorators).
  • Basic knowledge of object-oriented programming.

What you will learn in this course:

  • What decorators are, how they work, and how to create them.
  • How to stack them on top of each other.
  • How to define decorators with arguments.
  • How to use classes as decorators.

Note: Decorators are often created by library or framework developers, because they allow additional abstractions to be offered. Even if you don’t create many of your own decorators, it’s important to understand how they work because you’ll use many of them in your applications.


2.2 Functions as first-class objects (First-Class Objects)

In Python, everything is an object — including functions. We say that the functions are first-class citizens or first-class objects.

This means that everything you can do with objects like str or int, you can also do with a function object:

  • Assign them to a variable
  • Store them in a collection (list, dictionary, etc.)
  • Pass them as arguments to other functions

Assign a function to a variable

# first_class_objects.py
def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

plus = add           # On assigne l'objet fonction (sans parenthèses)
print(plus(2, 3))    # Appel via la nouvelle référence
print(add)           # Affiche l'adresse mémoire de l'objet add
print(plus)          # Affiche la même adresse — c'est le même objet

operations = [add, subtract]
print(operations[1](2, 3))   # Appel de subtract via la liste

Important: Note that plus = add assigns the function object itself, not the result of its call. There are no parentheses.

The variable plus is a new reference to the same function object in memory. Both identifiers add and plus point to the same object (same memory address).

Pass a function as an argument: Higher-Order Functions

A function that takes another function as an argument or returns a function is called a higher-order function.

# hof.py
def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

def calculator(operation, a, b):
    return operation(a, b)

print(calculator(add, 2, 3))        # 5
print(calculator(subtract, 2, 3))   # -1

Here, calculator is a higher-order function because it takes a function (operation) as an argument.


2.3 Returning internal functions from Higher-Order Functions

Local scope reminder

Each function call creates its own local scope. Parameters and variables defined inside a function are only accessible within that scope.

# local_scope.py
def add(a, b):
    print(a)
    return a + b

print(add(2, 3))
print(add(5, 5))
# print(a)  # NameError: name 'a' is not defined

Trying to access a from global scope causes a NameError. Additionally, once the function has completed its evaluation, the values ​​of its local variables are freed from memory by the garbage collector.

Inner Functions

It is entirely possible to define a function inside the body of another function. This function is called an inner function.

# inner_functions.py
def outer():
    
    def inner():
        print("Inner function")
    
    return inner

func = outer()
func()
print(func)

What is happening:

  1. Setting inner creates a new function object in memory, in the local scope of outer.
  2. outer returns this function object (without calling it).
  3. func = outer() creates a global reference to this inner function object.
  4. This global reference keeps the inner object alive, even after outer has finished executing.

Key rule: An inner function can be made available outside the outer function, provided you return it from the higher-order function and create a new reference to this location in memory.


2.4 Preserve non-local state with Closures

Free Nonlocal Variables

Let’s look at this more interesting example:

# closures.py
def power(exponent):

    def inner(base):
        return base ** exponent

    return inner

power_of_two = power(2)
power_of_three = power(3)

print(power_of_two(5))    # 25
print(power_of_three(5))  # 125

Analysis:

  • power is a higher-order function that takes an exponent as an argument and returns inner.
  • inner uses exponent, but it is not a local variable of inner (neither in its body nor as a parameter).
  • Python looks for exponent in the enclosing scope — which is the local scope of power.

The theoretical problem: When power(2) is called, the local scope of power is created with exponent = 2. The inner object is defined, then returned. Once power has finished, its local scope should normally be destroyed by the garbage collector. But then, how can inner still access exponent?

The answer: Closures

Python implements a mechanism called closure. When an inner function references variables in the enclosing scope, Python captures them in a special object attached to the inner function. These captured variables are called *free nonlocal variables.

  • They are not local to the function itself.
  • They are local to the enclosing scope.

A closure is therefore a function which:

  1. Uses non-local variables.
  2. Encloses (encloses) the function itself as well as the extended scope of these non-local variables.

Thanks to closures, even after the scope of power is destroyed, the value of exponent remains accessible via the closure of inner.


2.5 Decorate functions

Now that we understand closures and higher-order functions, we can understand how decorators are implemented.

Defining a decorator

A decorator is a higher-order function that takes another function as an argument and returns a closure.

Practical example

Let’s imagine that we need to send emails to interns, and that each email must follow a label: start with “Dear interns,” and end with “Best regards, your new boss”. Rather than repeating this code in each message function, we create a decorator.

# decorators.py
def email_decorator(func):
    def wrapper():
        print("Dear interns,")
        func()
        print("Best regards,")
        print("your new boss")
    return wrapper

@email_decorator
def greeting_message():
    print("Welcome to your new job!")

# Ce que fait réellement la syntaxe @email_decorator :
# greeting_message = email_decorator(greeting_message)
greeting_message()

What the @email_decorator syntax does:

  1. Python passes the original function object (greeting_message) to the decorator.
  2. Python reassigns the name greeting_message to the closure returned by the decorator (here wrapper).

Advantages of this approach:

  • separation of concerns: Email label logic is isolated in the decorator.
  • Maintainability: if you change the way you write emails, you only change one place.

The wrapper closure is what is actually called when writing greeting_message(). It then calls the original function via the non-local variable func.


2.6 Module 2 Summary

ConceptDescription
First-class objectsFunctions can be assigned to variables, stored in collections, and passed as arguments.
Higher-order functionsFunctions that take another function as an argument or that return a function.
Inner functionsFunctions defined inside the body of another function.
Free nonlocal variablesVariables used in an inner function that come from the enclosing scope.
ClosuresFunctions that use non-local variables — they “close” the function with the extended scope of those variables.
DecoratorsHigher-order functions which take a function, create an internal closure adding additional behavior, and return this closure.

The usual steps for applying a decorator are:

  1. Assign the new closure to the identifier of the original function: func = decorator(func)
  2. Or use the @decorator syntax directly on the function definition.

3. Implement function decorators


3.1 Going deeper into decorators

The call stack and frames

When Python encounters a decorator, it handles function calls using the call stack. The call stack keeps track of all active subroutines or functions in a program, along with their return addresses, local variables, and parameters.

  • A function call pushes a new frame onto the call stack.
  • A frame contains all the information needed to evaluate a function.

Closures and __closure__

The closure returned by a decorator has a special __closure__ attribute which contains cell objects — each cell contains the value of a captured non-local variable.

# 03/demos/1. Digging Deeper into Decorators/script.py
def email_decorator(func):
    def wrapper():
        print("Dear interns,")
        func()
        print("Best regards,")
        print("your new boss")
    return wrapper

@email_decorator
def greeting_message():
    print("Welcome to your new job!")

print(greeting_message.__closure__)
print(greeting_message.__closure__[0])
print(greeting_message.__closure__[0].cell_contents)

# L'adresse mémoire de la fonction wrapper
print(f"0x{id(greeting_message):x}")
  • greeting_message.__closure__ returns a tuple of cell objects.
  • .cell_contents provides access to the captured value (here, the original greeting_message function).
  • The greeting_message identifier now points to the wrapper closure.

3.2 Decorating functions with arguments

The problem

If the decorated function accepts arguments, but the decorator’s wrapper does not accept any, we get a TypeError.

@email_decorator
def greeting_message(x):
    print(f"Welcome to your new {x}!")

greeting_message("desk")
# TypeError: wrapper() takes 0 positional arguments but 1 was given

The solution: *args and **kwargs

To make decorators reusable with any function, we use the special parameters *args and **kwargs:

  • *args: captures all positional arguments in a tuple.
  • **kwargs: captures all named arguments in a dict.
# 03/demos/2. Decorating Functions with Arguments/script.py
def email_decorator(func):
    def wrapper(*args, **kwargs):
        # args = ("desk",)
        # kwargs = {}
        print("Dear interns,")
        func(*args, **kwargs)
        # func("desk")  ← équivalent après unpacking
        print("Best regards,")
        print("your new boss")
    return wrapper

@email_decorator
def greeting_message(x):
    print(f"Welcome to your new {x}!")

greeting_message("desk")

Behavior of *args and **kwargs:

  • In the definition (parameters): they collect the arguments.
  • In the call (arguments): they unpack the values.

Thus, func(*args, **kwargs) passes all arguments received by wrapper to the original function, regardless of their number.


3.3 Keep original function metadata

Introspection

Introspection is the ability of an object to analyze its own attributes at runtime. Many tools and libraries use this functionality to work with object metadata.

The metadata of a function includes:

  • __name__: the name of the function.
  • __doc__: the docstring of the function.

The problem of metadata loss

When decorating a function, the name of the original function now points to the wrapper closure. So if we inspect __name__ or __doc__, we get the metadata of wrapper, not that of the original function.

# Sans functools.wraps — problème de métadonnées
def email_decorator(func):
    def wrapper(*args, **kwargs):
        print("Dear interns,")
        func(*args, **kwargs)
        print("Best regards,")
        print("your new boss")
    return wrapper

@email_decorator
def greeting_message(x):
    """Function displays a greeting message"""
    print(f"Welcome to your new {x}!")

print(greeting_message.__name__)  # wrapper  ← incorrect !
print(greeting_message.__doc__)   # None     ← incorrect !

The original function metadata is still intact. The problem is that the greeting_message id now points to wrapper, whose __name__ is “wrapper” and which has no docstring.

The solution: functools.wraps

The functools module provides the @wraps decorator, which copies the metadata from the original function to the closure wrapper.

# 03/demos/3. Retaining Function's Original Metadata/script.py
from functools import wraps

def email_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Dear interns,")
        func(*args, **kwargs)
        print("Best regards,")
        print("your new boss")
    return wrapper

@email_decorator
def greeting_message(x):
    """Function displays a greeting message"""
    print(f"Welcome to your new {x}!")

print(greeting_message.__name__)  # greeting_message ← correct !
print(greeting_message.__doc__)   # Function displays a greeting message ← correct !

Best practice: Always use @wraps(func) in the wrapper of your decorators to preserve transparency and allow correct introspection.


3.4 Commonly used decorators in Python

Besides custom decorators, the most common ones come from the Python standard library or external frameworks.

@classmethod

By default, any method defined in a class is an instance method which receives the instance (self) as its first parameter. The @classmethod decorator transforms a method into a class method, automatically passing it the class itself (cls) as the first parameter.

# 03/demos/4. Commonly Used Decorators in Python/classmethod.py
class MyClass:
    @classmethod
    def my_class_method(cls):
        print("This is a class method.")

MyClass.my_class_method()  # Appel direct depuis la classe, sans instance

There is also @staticmethod, similar in functionality but less used.

@property

The @property decorator is the Python alternative to traditional getters and setters. Rather than exposing instance attributes directly, we create property methods.

# 03/demos/4. Commonly Used Decorators in Python/property.py
class MyClass:
    def __init__(self, x):
        self._x = x

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        if value < 5:
            self._x = value

c = MyClass(3)
c.x = 2         # Appelle implicitement le setter — vérifie value < 5
print(c.x)      # Appelle implicitement le getter — retourne self._x

From the outside, accessing c.x looks like ordinary attribute access, but Python invokes the decorated methods behind the scenes. This allows you to implement transparent validation logic.

@lru_cache (Least Recently Used Cache)

The @lru_cache decorator of the functools module caches the results of a function. When the function is called back with the same arguments, the cached result is returned directly without recalculation.

# 03/demos/4. Commonly Used Decorators in Python/lru_cache.py
from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(100))

Without @lru_cache, calculating fib(100) via recursion would be exponentially slow. With caching, each intermediate value is calculated only once.

  • maxsize=None: the cache has no size limit.
  • maxsize=128 (default): only keeps the 128 most recent calls.

3.5 Implement your own decorators

Logging decorator

The standard logging library allows you to create a simple logging decorator, without external dependencies.

# 03/demos/5. Implementing Your Own Decorators/log.py
import logging
from functools import wraps

logging.basicConfig(level=logging.INFO)

def log_function_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"Calling function '{func.__name__}' with args: {args} and kwargs: {kwargs}")
        return func(*args, **kwargs)
    return wrapper

@log_function_call
def add(a, b):
    return a + b

print(add(2, 3))

Important points:

  • logging.basicConfig(level=logging.INFO) globally configures the logging level.
  • The wrapper returns the result of func(*args, **kwargs).

Best practice: Always return the value of the original function call in the wrapper, even if it is None. This ensures that decorated functions can return values, even if they don’t return any yet.

Performance measurement decorator (timing)

# 03/demos/5. Implementing Your Own Decorators/timing.py
import time
from functools import wraps

def time_function(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed_time = time.perf_counter() - start_time
        print(f"Function '{func.__name__}' took {elapsed_time:.6f} seconds to run.")
        return result
    return wrapper

@time_function
def slow_function():
    time.sleep(1)

slow_function()
  • time.perf_counter() is the high resolution counter recommended for measuring short durations.
  • The function result is stored in result and then returned after displaying the elapsed time.

3.6 Module 3 Summary

ConceptDescription
Call stack / FramesStructure that manages all active function calls and their contexts.
*args / **kwargsAllow decorators to work with functions having any number of arguments.
functools.wrapsCopies the original function’s metadata to the closure, preserving __name__, __doc__, etc.
@classmethodTransforms a method into a class method, receives cls instead of self.
@propertyAlternative to getters/setters, allows transparent validation of attributes.
@lru_cacheCaches function results to avoid redundant recalculations.
Logging decoratorUses logging.info to log function calls.
Timing DecoratorUses time.perf_counter to measure execution time.

4. Use advanced decorator workflows


4.1 Handling arguments with Decorator Factories

Reminder: decorator without arguments

@decorator
def my_function():
    pass
# Équivalent à :
# my_function = decorator(my_function)

Decorator with argument

When you want to pass an argument to the decorator itself (and not to the decorated function), you need to add an extra layer above the decorator.

@decorator_factory(some_arg)
def my_function():
    pass
# Équivalent à :
# my_function = decorator_factory(some_arg)(my_function)

The resolution is done in two steps:

  1. decorator_factory(some_arg) is called and returns a decorator.
  2. This decorator is then applied to my_function.

This is why we call this type of function a decorator factory: the external function receives the argument and manufactures a decorator that it returns.

Generic structure:

def decorator_factory(arg):
    def decorator(func):           # Le vrai décorateur
        def wrapper(*args, **kwargs):
            # ... logique supplémentaire utilisant `arg` ...
            return func(*args, **kwargs)
        return wrapper
    return decorator               # La factory retourne le décorateur

Note: “decorator factory” is not an official Python documentation term, but it is technically correct and commonly used.


4.2 Implement decorators with arguments

Here is the example of the email decorator modified to accept a fromWho argument:

# 04/demos/2. Implementing Decorators with Arguments/script.py
from functools import wraps

def email_decorator(fromWho):
    def _email_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print("Dear interns,")
            func(*args, **kwargs)
            print("Best regards,")
            print(f"your new {fromWho}")
        return wrapper
    return _email_decorator

@email_decorator(fromWho="team leader")
def greeting_message(x):
    """Function displays a greeting message"""
    print(f"Welcome to your new {x}!")

greeting_message("job")

Output:

Dear interns,
Welcome to your new job!
Best regards,
your new team leader

Execution flow:

  1. email_decorator(fromWho="team leader") is called → returns _email_decorator.
  2. _email_decorator(greeting_message) is called → returns wrapper.
  3. greeting_message is now linked to wrapper.

The non-local variable fromWho is captured in the wrapper closure via two levels of enclosing scope.


4.3 Stack multiple decorators on a single function

It is possible to apply several decorators to the same function by *stacking them on top of each other.

Syntax

@decorator1
@decorator2
def my_function():
    pass

# Équivalent à :
# my_function = decorator1(decorator2(my_function))

Evaluation order

The order is bottom to top when applying, but top to bottom when executing:

  • decorator2 is applied first to my_function.
  • decorator1 receives the closure returned by decorator2.
  • When called, decorator1 executes first.

Important: The order of the decorators is crucial. The highest decorator is always evaluated first at runtime.

Example with company_info and email_decorator

# 04/demos/3. Stacking Multiple Decorators on a Single Function/script.py
from functools import wraps

def company_info(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
        print("CompanyName (LLC) Street 22 company@mail.com")
    return wrapper

def email_decorator(fromWho):
    def _email_decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print("Dear interns,")
            func(*args, **kwargs)
            print("Best regards,")
            print(f"your new {fromWho}")
        return wrapper
    return _email_decorator

@company_info
@email_decorator(fromWho="team leader")
def greeting_message(x):
    """Function displays a greeting message"""
    print(f"Welcome to your new {x}!")

greeting_message("job")

Output:

Dear interns,
Welcome to your new job!
Best regards,
your new team leader
CompanyName (LLC) Street 22 company@mail.com

Execution flow:

  1. email_decorator(fromWho="team leader"))(greeting_message) → produces wrapper_email.
  2. company_info(wrapper_email) → produces wrapper_company.
  3. greeting_message now points to wrapper_company.
  4. When calling: wrapper_company executes, calls wrapper_email, which calls the original function.

4.4 Custom decorators in Flask

Web frameworks like Flask massively exploit decorators to offer a simple and elegant programming interface.

The @route decorator

@app.route("/")
def index():
    return "<h1>Welcome to my website!</h1>"

@app.route("/") is a decorator with argument (the URL path). When someone visits the home page (/), Flask calls the index function. This is a great example of frameworks using decorators to provide a simple programming interface.

Custom decorators for authentication and authorization

In a Flask application, you can create your own decorators to manage security:

@login_requiredauthentication decorator:

@app.route("/logout")
@login_required
def logout():
    # Logique de déconnexion
    pass

Order of decorators here:

  • @app.route is at the top because it is the one that receives HTTP requests from the client.
  • @login_required is below: after receiving the request, we first check if the user is authenticated.

@permission_required("admin")permission decorator (with argument):

@app.route("/admin")
@login_required
@permission_required("admin")
def admin_panel():
    # Page d'administration
    pass

This example illustrates a real decorator chain where order is critical:

  1. Flask intercepts HTTP request (@app.route`).
  2. We check if the user is logged in (“@login_required`).
  3. We check if the user has the necessary permissions (@permission_required("admin")).

4.5 Summary of Module 4

ConceptDescription
Decorator factoryExternal function that receives decorator arguments and returns a decorator.
3-level structureFactory → Decorator → Wrapper, allowing access to arguments in the internal closure.
StackingStack multiple decorators on a function. The topmost decorator runs first.
Flask @routeDecorator with argument to associate a URL with a function.
@login_requiredCustom authentication decorator, placed under @route.
@permission_requiredAuthorization decorator with argument, to control access by role.

5. Decorate classes and Class Decorators


5.1 Using classes as decorators

A class can act as a decorator by implementing two special methods:

  • __init__: receives decorator arguments.
  • __call__: receives the function to decorate and returns a closure.

Correspondence with decorator factory

Decorator factoryClass Decorator
Factory function (arg)__init__(self, arg)
Internal decorator function__call__(self, func)
Closure wrapperDefined in __call__
# 05/demos/1. Using Classes as Decorators/script.py
from functools import wraps

class EmailDecorator:
    def __init__(self, fromWho):
        self.fromWho = fromWho          # Stocke l'argument dans un attribut d'instance

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            print("Dear interns,")
            func(*args, **kwargs)
            print("Best regards,")
            print(f"your new {self.fromWho}")   # Accès via self, pas comme variable non-locale
        return wrapper

# Équivalent commenté avec decorator factory :
# def email_decorator(fromWho="boss"):
#     def _email_decorator(func):
#         @wraps(func)
#         def wrapper(*args, **kwargs):
#             print("Dear interns,")
#             func(*args, **kwargs)
#             print("Best regards,")
#             print(f"your new {fromWho}")
#         return wrapper
#     return _email_decorator

@EmailDecorator(fromWho="team leader")
def greeting_message(x):
    """Function displays a greeting message"""
    print(f"Welcome to your new {x}!")

greeting_message("job")

Resolution of @EmailDecorator(fromWho="team leader"):

  1. EmailDecorator(fromWho="team leader") is called → creates an instance with self.fromWho = "team leader".
  2. This instance is called with the function greeting_message__call__(greeting_message) is invoked.
  3. __call__ returns the wrapper closure.
  4. greeting_message now points to wrapper.

Key difference: In a decorator factory, the argument is accessible as a non-local variable. In a class, it is accessible via self.fromWho — an instance attribute.


5.2 property is a Class Decorator

Python’s built-in property class is actually a class, not a function, although its lowercase nomenclature can be confusing.

Signature of property

property(fget=None, fset=None, fdel=None, doc=None)

All parameters are optional. We generally provide the first two.

Protocol Descriptor

The property class implements the descriptor protocol. A descriptor object is an object whose class implements the special methods __get__, __set__, and/or __delete__.

When you access an attribute via obj.x and x is a descriptor, Python does not return the value directly — it calls the descriptor’s __get__ method.

class MyClass:
    def __init__(self, x):
        self._x = x

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        if value < 5:
            self._x = value

How ​​@property works behind the scenes:

  1. @property on def x(self) is equivalent to: x = property(fget=x). This creates a property object with the getter method.
  2. @x.setter on def x(self, value) is equivalent to: x = x.setter(x). This creates a new property object which incorporates the setter.

The x attribute in the class is a descriptor object of type property. When Python sees obj.x, it invokes property.__get__, which calls the getter method.

Educational objective

The goal of this lesson is to train the mind to see through the syntax of decorators and understand how they are actually implemented. When you see @property, think of a class descriptor object that intercepts access to attributes.


5.3 Decorating classes

Until now, decorators applied to functions. But the @decorator syntax also works with classes.

@decorator
class MyClass:
    pass
# Équivalent à :
# MyClass = decorator(MyClass)

The decorator receives the class object as an argument and must return a class (usually the same, modified).

Monkey Patching

Monkey patching consists of dynamically adding attributes to an object at runtime. This is the main use of class decorators.

# 05/demos/3. Decorating Classes/script.py
def add_speech(cls):
    cls.speak = lambda self: f"Hello, I'm a {self.__class__.__name__} instance"
    return cls   # ← Important : ne pas oublier de retourner la classe !

@add_speech
class SomeClass:
    pass

# SomeClass = add_speech(SomeClass)

obj = SomeClass()
print(obj.speak())

Common error: If the decorator does not return the class, the SomeClass variable points to None (implicit return), and the instantiation fails with TypeError: 'NoneType' object is not callable.

Class register

Another classic use case: automatically register classes in a registry.

# 05/demos/3. Decorating Classes/registry.py
registry = {}

def register_class(cls):
    registry[cls.__name__] = cls
    return cls

@register_class
class SomeClass:
    pass

# registry == {"SomeClass": <class 'SomeClass'>}

This pattern is used in frameworks to auto-discover and register components (handlers, plugins, models, etc.).


5.4 Standard library decorators

@total_ordering (module functools)

When you create classes whose instances must be compared with comparison operators (<, >, <=, >=, ==, !=), you should normally define all these special methods (dunder methods).

@total_ordering simplifies this: just define __eq__ and one of the methods __lt__, __le__, __gt__, or __ge__. The decorator dynamically infers and attaches the other missing methods.

from functools import total_ordering

@total_ordering
class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

    def __eq__(self, other):
        return self.grade == other.grade

    def __lt__(self, other):
        return self.grade < other.grade

# total_ordering génère automatiquement __le__, __gt__, __ge__

Warning: @total_ordering may have performance drawbacks. Use with discretion.

@dataclass (module dataclasses)

Many Python classes follow the same boilerplate:

  • __init__: takes arguments and assigns them to instance attributes.
  • __repr__: converts the instance to a readable string.
  • __eq__: compares two instances.

The @dataclass decorator automatically generates all this boilerplate from the class’s type annotations.

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float
    z: float = 0.0   # Valeur par défaut

p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)
print(p1)         # Point(x=1.0, y=2.0, z=0.0)
print(p1 == p2)   # True — __eq__ généré automatiquement

What @dataclass automatically generates:

  • __init__: initialization of attributes.
  • __repr__: readable string representation.
  • __eq__: equality comparison of attributes.

The decorator inspects class attributes with type annotations to know which attributes should be part of the generated __init__.


5.5 Module 5 Summary

ConceptDescription
Class decoratorClass implementing __init__ (receives args) and __call__ (receives function, returns a closure).
property is a classproperty implements the descriptor protocol; its objects intercept access to attributes.
Descriptor protocolInterface (__get__, __set__, __delete__) allowing an object to manage access to attributes.
Decorate classroomsThe decorator receives a class, can modify its attributes (monkey patching), and must return the class.
Monkey patchingDynamically adding attributes/methods to an object or class at runtime.
@total_orderingInfers missing comparison operators from __eq__ and another operator.
@dataclassAutomatically generate __init__, __repr__, __eq__ and other dunder methods.

6. General Summary

Architecture of a complete decorator

from functools import wraps

def decorator_with_arg(arg):               # Decorator factory (optionnel)
    def decorator(func):                   # Vrai décorateur
        @wraps(func)                       # Préserve les métadonnées
        def wrapper(*args, **kwargs):      # Closure qui enveloppe la fonction
            # Comportement AVANT
            result = func(*args, **kwargs) # Appel de la fonction originale
            # Comportement APRÈS
            return result                  # Retourner le résultat
        return wrapper
    return decorator

Summary of Fundamental Concepts

ConceptRole in decorators
First-class objectsFunctions can be passed as arguments and returned.
Higher-order functionsDecorator foundation: takes a function, returns a closure.
Inner functionsThe wrapper closure is defined inside the decorator.
Free nonlocal variablesfunc and arg are captured in the wrapper closure.
ClosuresThe closure keeps non-local variables alive after the end of the enclosing scope.
@wraps(func)Copy __name__, __doc__, etc. from the original function to the closure.
*args, **kwargsMakes the decorator generic, compatible with all function signatures.
Decorator factoryAdds a layer to allow arguments to the decorator itself.
StackingMultiple @decorator on a function — order of execution from top to bottom.
Class as decorator__init__ + __call__ replace factory + decorator.
Decorating classesThe decorator receives and returns a class (monkey patching).

Standard decorators to know

DecoratorModuleUsage
@wrapsfunctoolsPreserve metadata in custom decorators
@lru_cachefunctoolsCaching function calls
@total_orderingfunctoolsComplete comparison operators
@dataclassdataclassesGenerate data class boilerplate
@classmethodbuilt-inClass method (receives cls)
@staticmethodbuilt-inStatic method (no self or cls)
@propertybuilt-inTransparent getter/setter (descriptor)
@app.routeFlaskAssociate a URL with a view function

Search Terms

python · decorators · foundations · data · analysis · engineering · analytics · decorator · functions · function · arguments · closures · class · classes · implement · property · argument · custom · decorate · decorating · higher-order · metadata · objective · order

Interested in this course?

Contact us to book it or get a custom training plan for your team.