top of page

Decorators: One Decorator for all

Writer: Mic
Mic
Dec 23, 2025
4 min read

So far, everything we’ve looked at has revolved around function decorators. They’re a great entry point: small, self-contained, and easy to reason about once you get past the initial syntax hurdle.

In this installment, we’ll zoom out a little and switch to a different flavour altogether: class decorators.

What an LLM thinks this post is about when asked to visualise it.

We’ll look at our first example today, a class wrapper that decorates methods in bulk. This example builds directly on ideas we’ve already used with function decorators—wrapping, interception, and controlled side effects—but applies them at a slightly higher level of abstraction.


When not to use Class Decorators

Class decorators are powerful, but they’re also easy to overuse. The first red flag is surprise. If reading a class definition doesn’t give a reasonably accurate picture of how its instances behave, a class decorator may be hiding too much.

Another warning sign is fine-grained behaviour. If only one or two methods need special treatment, a class decorator is usually the wrong tool. Decorating those methods directly is clearer and more explicit.

Class decorators are also a poor fit when inheritance matters. Since decorators run once, at class creation time, they don’t naturally compose with subclassing.


Class Wrapper: All at once...

Our example is the “okay, now make it class-shaped” step: take the idea of a function decorator and apply it to every method of a class in one go. In other words, instead of writing

we want something closer to


The old decorator

To keep the moving parts familiar, we’ll start with the same timing decorator from part one (the one that wraps a function, measures perf_counter(), prints the runtime, and returns the result). So our code is the following decorator

This is precisely our code from the first part of our posts on decorators. It expects a callable (inner in this case) and returns a callable.


Change to a Class

A class decorator receives a class object and returns a class object instead. That means the “wrapping point” shifts from a single function to the class namespace—we inspect the attributes and decorate the methods we care about. In code this is

From a logic standpoint is completely straight forward. We go through all attributes of the class, look up if they are a dunder method, in which case we ignore them and otherwise if they are a method we apply our classic decorator to them.

Why did we need special handling for staticmethod and classmethod? Unlike regular instance methods, these are not callable in the usual sense when we inspect the class. Inside cls.__dict__, they appear as descriptor objects, not as functions. As a result, attr itself isn’t callable—the underlying function lives in attr.__func__.

To decorate them correctly, we first extract that function, apply our decorator to it, and then wrap the result again using staticmethod or classmethod. This way, we add our behavior without breaking the method’s original binding semantics.


Apply it to a Class

Let's create a simulated payment service class and decorate it

Since we deliberately skip decorating dunder methods, we leave them out of this example—they don’t add much to the core idea, but we will discuss them later. What remains are two regular instance methods, process_payment and refund, along with a staticmethod and a classmethod. When we call these methods, we can see that all of them are timed automatically, even though they differ in how they are bound and invoked.

As expected, all of the methods are timed correctly. Now let’s add the following dunder method to the class and observe how it behaves under the same decorator.

This will be triggered when we call

As we can see, calling the dunder method produces no timing output. That’s expected: our approach was intentionally heavy-handed, excluding all dunder methods from decoration. In practice, this is often too coarse. While many dunder methods are best left untouched, there are cases where you may want to keep specific ones in the mix and explicitly opt them in for decoration.


Modify for more Flexibility

To selectively allow certain dunder methods to be decorated, we can take a more nuanced approach. Instead of excluding all dunders outright, we modify our class decorator to support an explicit allowlist, opting in only the dunder methods we consider safe. The decorator then becomes something like the following.

We can now pass in a list of dunder methods to decorate by switching to the allowlist-based version of the decorator. So we would change the head of our class definition to

Instead of our previous version, we can now use this allowlist-based decorator. With that change, the final print call from above would be timed as well. In practice, however, it’s relatively rare that a dunder method should be decorated at all. Many of them are invoked implicitly and potentially quite frequently, so adding extra logic there can easily lead to noisy output or unintended side effects.


Final thoughts


Class decorators are even rarer than function decorators. There are a few obvious candidates where they make sense, such as:

  • Automatic logging: injecting structured logging into all public methods, optionally tagging logs with the class name or an instance identifier.

  • Feature flags at the class level: disabling or swapping out entire sets of methods based on environment flags (for example, using stub implementations in tests).

  • Context injection: wrapping all methods so they automatically execute inside a context manager, such as a transaction, tracing span, or request context.

Even in these scenarios, however, it’s often the case that you don’t actually want to wrap every method. When the behavior should apply only to specific functions, a regular function decorator is usually the more precise and appropriate tool.


Comments


bottom of page