Class Decorators

Decorators aren't just for functions — a decorator can also take a whole class and return a modified or wrapped version of it.

The @decorator syntax works above a class statement exactly as it does above a def: the decorator receives the class object itself (not an instance) and returns whatever should be bound to the class's name. This lets you inject attributes, wrap methods across an entire class, register classes, or even replace a class with something else entirely, all without touching the class's own body.

How Class Decoration Works

@decorator above class Foo: ... is equivalent to defining the class normally and then executing Foo = decorator(Foo). The decorator's argument is the class object (a type), created after the class body has fully executed — so all methods and class attributes already exist when the decorator runs.

add_repr receives the finished Point class, attaches a new __repr__ method to it, and returns the same class object — decoration mutates and returns cls rather than wrapping it.

def add_repr(cls):
    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
        return f"{cls.__name__}({attrs})"
    cls.__repr__ = __repr__
    return cls

@add_repr
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

print(Point(1, 2))  # Point(x=1, y=2)

Modifying vs Replacing a Class

A class decorator can take two broad shapes: mutate the class in place and return it unchanged (as above — the common, less surprising style), or return a completely different object bound to the original name (e.g. a subclass, a wrapper class, or even a non-class object). The second style is more powerful but easier to misuse, since anything relying on isinstance checks or the original class identity may break.

freeze returns an entirely different class (Frozen, a subclass of the original Point) bound to the name Point — instances now carry the extra immutability behavior, but Point is no longer the exact class originally defined.

When a class decorator returns a subclass rather than the same object, type(instance) is OriginalClass becomes False even though isinstance(instance, OriginalClass) stays True. Prefer the in-place-mutation style unless you specifically need this extra layer.

def freeze(cls):
    """Return a subclass that forbids attribute assignment after __init__."""
    class Frozen(cls):
        _frozen = False
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self._frozen = True
        def __setattr__(self, name, value):
            if getattr(self, "_frozen", False):
                raise AttributeError(f"cannot set {name!r} — instance is frozen")
            super().__setattr__(name, value)
    Frozen.__name__ = cls.__name__
    return Frozen

@freeze
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)
p.x = 5  # AttributeError: cannot set 'x' — instance is frozen

Decorating Every Method of a Class

A common, well-behaved use of class decorators is applying an existing function decorator (a related concept) to every callable attribute of a class — for example, wrapping every method with logging or timing without editing each method individually.

log_all_methods walks the class's own namespace (vars(cls)), skips dunder methods to avoid breaking protocol methods, and replaces each remaining callable with its logged version.

import functools

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"calling {func.__qualname__}")
        return func(*args, **kwargs)
    return wrapper

def log_all_methods(cls):
    for name, value in vars(cls).items():
        if callable(value) and not name.startswith("__"):
            setattr(cls, name, log_calls(value))
    return cls

@log_all_methods
class Calculator:
    def add(self, a, b):
        return a + b
    def sub(self, a, b):
        return a - b

Calculator().add(2, 3)  # calling Calculator.add

Class Decorators for Registration

Another idiomatic use is registering classes into a lookup table as a side effect of decoration — common in plugin systems, command dispatchers, and ORMs. The decorator returns the class unchanged but records it somewhere for later lookup by name.

register("json") is a decorator factory (same pattern as function decorator factories) whose inner 'decorator' registers cls in HANDLERS as a side effect and returns cls unchanged.

HANDLERS = {}

def register(name: str):
    def decorator(cls):
        HANDLERS[name] = cls
        return cls
    return decorator

@register("json")
class JsonHandler:
    def handle(self, data):
        return f"json: {data}"

print(HANDLERS["json"]().handle({"a": 1}))  # json: {'a': 1}

dataclasses.dataclass: A Standard Library Class Decorator

The standard library's dataclasses.dataclass (3.7+) is the most widely used class decorator: applied to a class whose body declares annotated fields, it auto-generates __init__, __repr__, __eq__, and optionally ordering/hashing methods, mutating the class in place and returning it — exactly the pattern shown above, just more elaborate.

dataclass reads the class's annotations (x: int, y: int) and generates the boilerplate methods based on them — Point remains an ordinary class, just with methods attached by the decorator.

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1)          # Point(x=1, y=2) — __repr__ generated
print(p1 == p2)     # True — __eq__ generated

Appropriate Use Cases

Class decorators are the right tool when the modification applies uniformly to the whole class and doesn't need to participate in the inheritance chain in a structural way — adding a dunder method, registering the class, freezing instances, wrapping every method. When the modification needs to interact deeply with subclassing (e.g. controlling how EVERY subclass, not just this one class, is created), a metaclass is the more appropriate (and more complex) tool — that is a distinct, separate topic from class decorators.

Rule of thumb: reach for a class decorator first; only reach for a metaclass when you specifically need to influence subclass creation itself, which a class decorator (applied only to one class, not automatically to its subclasses) cannot do.

Mental Model

Think of a class decorator as running a one-time 'finishing pass' over an already fully-built class object — like a post-processing step in a build pipeline. It never runs per-instance; it runs exactly once, right after the class statement finishes executing, and whatever it returns is what the class's name will refer to from then on.

Nothing about class decoration happens per-instance; every instance of the resulting class simply reflects whatever the one decoration pass produced.

class Foo:          # 1. class body executes fully, building the class object
    ...

Foo = decorator(Foo)  # 2. decorator runs ONCE, receives the finished class, returns something
                       # 3. 'Foo' now refers to whatever decorator returned

Key Takeaways

@decorator above a class statement is sugar for Cls = decorator(Cls), run once after the class body finishes executing.

A class decorator receives the class object itself, not an instance.

Mutating the class and returning it unchanged is the safer, more common style; returning a different object (e.g. a subclass) can break identity checks.

Typical uses: attaching/overriding methods across the whole class, wrapping every method, registering the class in a lookup table.

dataclasses.dataclass is the standard library's flagship example of this pattern.

Reach for a metaclass instead only when the behavior must apply automatically to subclasses too — a separate, more advanced tool.

def add_id(cls): class Wrapped(cls): pass return Wrapped @add_id class User: pass u = User() print(type(u) is User) # False — surprising identity break def add_id(cls): cls.id_prefix = "USR-" return cls @add_id class User: pass u = User() print(type(u) is User) # True

Wrapping the class in a subclass and returning that instead of the original silently changes type(instance) identity; prefer mutating and returning the same class object unless a subclass is specifically required.

def log_all_methods(cls): for name, value in vars(cls).items(): if callable(value): setattr(cls, name, log_calls(value)) # also wraps __init__, __repr__, etc. return cls def log_all_methods(cls): for name, value in vars(cls).items(): if callable(value) and not name.startswith("__"): setattr(cls, name, log_calls(value)) return cls

Wrapping dunder methods indiscriminately can break protocols (e.g. wrapping __init__ or __eq__ with a decorator not designed for them); skip name-mangled/dunder attributes unless specifically intended.

@register("json") # register forgot to return cls def decorator(cls): HANDLERS["json"] = cls # missing return cls class JsonHandler: pass def register(name): def decorator(cls): HANDLERS[name] = cls return cls # must return cls, or the name becomes None return decorator

A class decorator's return value replaces the class's name; forgetting to return cls makes the decorated name bind to None (the decorator function's implicit return value) instead of the class.

What object does a class decorator receive as its argument?

The fully-constructed class object itself (a type), after the class body has finished executing — not an instance of the class.

What is the safer general style for a class decorator: mutating and returning the same class, or returning a different object?

Mutating the class in place and returning the same object is generally safer, since it preserves object identity (type(instance) is OriginalClass) that code elsewhere may rely on; returning a different object (like a subclass) can silently break such checks.

How would you use a class decorator to register every decorated class into a dictionary keyed by name?

Write a decorator factory register(name) whose inner decorator function stores cls in a module-level dict under that name as a side effect, then returns cls unchanged so the class itself is untouched.

Why does dataclasses.dataclass need to be a class decorator rather than, say, a base class to inherit from?

Because it needs to read the class's own annotations after the class body has executed and then generate/attach methods (__init__, __repr__, __eq__) onto that specific class — a decorator gets exactly that post-construction hook, which plain inheritance does not provide in the same way.

When should you reach for a metaclass instead of a class decorator?

Only when the behavior needs to apply automatically to every subclass at the moment each subclass is created, not just to the one decorated class — class decorators only affect the specific class they're applied to.

What happens if a class decorator forgets to return anything?

The decorated name gets rebound to None (a function with no explicit return returns None), so ClassName afterward refers to None instead of the class, and instantiating it fails.

Why should a decorator that wraps every method of a class typically skip names starting with double underscores?

Dunder methods implement Python's object protocols (like __init__, __eq__, __repr__) with specific expected signatures and semantics; wrapping them with a generic logging/timing wrapper can break how Python invokes them or produce confusing behavior.

Write a class decorator singleton that makes a class return the same instance every time it's constructed, regardless of arguments passed after the first call.

def singleton(cls): instances = {} def get_instance(*args, **kwargs): if cls not in instances: instances[cls] = cls(*args, **kwargs) return instances[cls] return get_instance @singleton class Config: def __init__(self): self.settings = {} a = Config() b = Config() print(a is b) # True singleton replaces the class name with get_instance, a closure over 'instances'; the first call constructs and caches the real instance, and every subsequent call returns the cached one instead of building a new object.

What does this print?

WIDGET. uppercase_name mutates the class by attaching a new class attribute 'label' set to the uppercased class name, then returns the same class object unchanged otherwise.

Write a class decorator add_str_from_repr that gives a class a __str__ method equal to its __repr__, without modifying __repr__ itself.

def add_str_from_repr(cls): def __str__(self): return self.__repr__() cls.__str__ = __str__ return cls @add_str_from_repr class Point: def __init__(self, x, y): self.x, self.y = x, y def __repr__(self): return f"Point({self.x}, {self.y})" print(str(Point(1, 2))) # Point(1, 2)

The decorator attaches a new __str__ method to the class that simply delegates to whatever __repr__ the class already defines, leaving __repr__ itself untouched.

What does this print, and why?

None. broken mutates cls but has no explicit return statement, so it implicitly returns None; @broken then rebinds the name App to that None, discarding the actual class object.

def uppercase_name(cls):
    cls.label = cls.__name__.upper()
    return cls

@uppercase_name
class widget:
    pass

print(widget.label)
def broken(cls):
    cls.version = 1

@broken
class App:
    pass

print(App)