When your decorator needs to track state across calls - counting invocations, caching results, or managing rate limits - a class-based decorator provides cleaner organization than nested closures with nonlocal variables.

Decorating Methods

When decorating methods, handle self carefully (use functools.wraps and proper signatures).

basic.py
# Basic class decorator

from functools import wraps


class LogCalls:
    def __init__(self, func):
        wraps(func)(self)
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f"calling {self.func.__name__}")
        return self.func(*args, **kwargs)


@LogCalls
def greet(name):
    return f"hello, {name}"


print(greet("Alice"))

stateful.py
# Stateful class decorator

from functools import wraps


class CountCalls:
    def __init__(self, func):
        wraps(func)(self)
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} called {self.count} time(s)")
        return self.func(*args, **kwargs)


@CountCalls
def task():
    return "done"


task()
task()
task()

with_params.py
# Class decorator with parameters

from functools import wraps


class Repeat:
    def __init__(self, times):
        self.times = times

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(self.times):
                result = func(*args, **kwargs)
            return result

        return wrapper


@Repeat(times=3)
def greet(name):
    print(f"hello, {name}")


name = 
greet(name)

# Class decorator with parameters

from functools import wraps


class Repeat:
    def __init__(self, times):
        self.times = times

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(self.times):
                result = func(*args, **kwargs)
            return result

        return wrapper


@Repeat(times=3)
def greet(name):
    print(f"hello, {name}")


name = 
greet(name)

# Class decorator with parameters

from functools import wraps


class Repeat:
    def __init__(self, times):
        self.times = times

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(self.times):
                result = func(*args, **kwargs)
            return result

        return wrapper


@Repeat(times=3)
def greet(name):
    print(f"hello, {name}")


name = 
greet(name)

cache.py
# Cache decorator

from functools import wraps


class Memoize:
    def __init__(self, func):
        wraps(func)(self)
        self.func = func
        self.cache = {}

    def __call__(self, *args):
        if args not in self.cache:
            print(f"computing {self.func.__name__}{args}")
            self.cache[args] = self.func(*args)
        return self.cache[args]


@Memoize
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)


print("fib(5):", fib(5))
print("fib(6):", fib(6))

method_decorator.py
# Decorator on class methods

from functools import wraps


class LogMethod:
    def __init__(self, func):
        wraps(func)(self)
        self.func = func

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        from functools import partial as _partial
        return _partial(self.__call__, obj)

    def __call__(self, instance, *args, **kwargs):
        print(f"calling {self.func.__name__} on {instance.__class__.__name__}")
        return self.func(instance, *args, **kwargs)


class Calculator:
    @LogMethod
    def add(self, a, b):
        return a + b


calc = Calculator()
print("result:", calc.add(2, 3))

When to Use Classes

  • Stateful decorators: need to track calls, cache results, etc.
  • Complex logic: easier to organize in methods
  • Reusable configuration: combine with __init__ parameters
__call__ method - makes class instances callable, enabling classes to work as decorators
stateful decorator - a decorator that maintains state between calls using instance attributes
method decorator - decorating instance methods requires careful handling of `self`

Exercise: practical.py

Create a call-counting decorator that tracks function usage