Class decorators
Overview
A class decorator is a function that takes a class object as an argument and returns a modified class. Instead of modifying instances, it alters the class definition itself. The class name is rebound to whatever the decorator returns, applied once at definition time. Think of it like a car factory adding a custom spoiler to every car model before any cars are actually produced. When writing a class decorator, you can: - Mutate the class directly to add attributes or methods - Register the class in a global registry - Return an entirely new wrapper class Returning a wrapper class creates an identity trap that hides the original class metadata (like `__name__` and `__doc__`). def wrap(cls): class Wrapper: pass return Wrapper @wrap class Job: pass print(Job.__name__) # Prints 'Wrapper'
It reduces boilerplate when you need to apply the same structural changes across many classes. Common use cases include: - Adding standard methods like `__init__` or `__repr__` - Registering classes to a central store or registry - Enforcing class-level constraints
Where used: `dataclasses` (`@dataclass`), `pytest` (`@pytest.mark`), `SQLAlchemy`
Why learn this
- Understand how `@dataclass` magically adds `__init__` and `__repr__` to your classes
- Learn to register classes automatically upon definition
Code walkthrough
def add_id(cls):
cls.id_field = 'default_id'
return cls
@add_id
class User:
pass
print(User.id_field)
Focus: cls.id_field = 'default_id' # The class object is mutated directly
Aha moment
def oops_decorator(cls):
print('Decorated!')
# forgot to return cls
@oops_decorator
class App:
pass
print(type(App))
Prediction: What is the type of `App`?
Common guess: `<class 'type'>`
Because `oops_decorator` didn't return anything, it implicitly returned `None`. The name `App` is now bound to `None`, not the class.
Common mistakes
- Forgetting to return the class: If your class decorator does not return the class object, the class name will be bound to `None` (or whatever you returned). This causes an unexpected `TypeError` when you try to instantiate it.
- Mutating the original class unexpectedly: Class decorators usually mutate the class in-place and return it. If multiple parts of the codebase expect the unmodified class, this can cause hidden bugs.
Glossary
- boilerplate
- Standard pieces of code that have to be written over and over again with little alteration, e.g., `def __init__(self, x): self.x = x`.
- mutate
- To change or alter the internal state or data of an object, e.g., `user.age += 1`.
Recall questions
- What argument does a class decorator function receive?
- Why would you use a class decorator instead of standard inheritance?
- What happens if a class decorator returns a new wrapper class instead of mutating the original class?
Understanding checks
What does this code print?
20
The class decorator receives the class object, modifies its `value` attribute directly to `20`, and returns it.
Why would you choose a class decorator over modifying instances directly in the `__init__` method?
Class decorators alter the class definition itself once (e.g., adding standard boilerplate or registering the class globally), rather than running logic every time a new instance is created.
This reinforces the mental model that a class decorator modifies the blueprint, not the individual objects produced from it.
Practice tasks
Modify Plugin Registration
The `register_plugin` decorator currently appends the class name to `PLUGINS`. Modify it so that it appends the actual class object to `PLUGINS` instead, and ensure it returns the `cls` object.
Challenge
Adding a class-level timestamp
Create a class decorator `add_timestamp` that adds a class attribute `created_at` to the decorated class. Set its value to `'2023-01-01'`. Apply this decorator to a class `Record` and print `Record.created_at`.
Continue learning
Previous: Function Decorators
Previous: Everything is an object in Python