python
decorators
classes
object-oriented-programming
python-tutorial

Python decorators in classes

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Python decorators can be used both on class methods and as class-based decorators. When decorating methods inside a class, the decorator must account for the self parameter. Built-in decorators like @staticmethod, @classmethod, and @property modify how methods bind to instances. You can also write custom decorators for methods (e.g., for logging, timing, or access control) and implement decorators as classes using the __call__ method.

Built-in Class Decorators

@staticmethod

Removes the implicit self or cls parameter. The method behaves like a regular function that lives in the class namespace:

python
1class MathUtils:
2    @staticmethod
3    def add(a, b):
4        return a + b
5
6# Called without an instance
7print(MathUtils.add(3, 5))  # 8
8
9# Also works on instances
10m = MathUtils()
11print(m.add(3, 5))  # 8

@classmethod

Receives the class (cls) instead of the instance (self) as the first argument:

python
1class User:
2    def __init__(self, name, email):
3        self.name = name
4        self.email = email
5
6    @classmethod
7    def from_string(cls, user_string):
8        name, email = user_string.split(',')
9        return cls(name.strip(), email.strip())
10
11user = User.from_string("Alice, [email protected]")
12print(user.name)   # Alice
13print(user.email)  # [email protected]

cls refers to the class itself, so from_string works correctly with subclasses too.

@property

Defines getter, setter, and deleter methods that look like attribute access:

python
1class Circle:
2    def __init__(self, radius):
3        self._radius = radius
4
5    @property
6    def radius(self):
7        return self._radius
8
9    @radius.setter
10    def radius(self, value):
11        if value < 0:
12            raise ValueError("Radius cannot be negative")
13        self._radius = value
14
15    @property
16    def area(self):
17        import math
18        return math.pi * self._radius ** 2
19
20c = Circle(5)
21print(c.radius)  # 5 (calls getter)
22print(c.area)    # 78.54 (computed property)
23c.radius = 10    # Calls setter
24# c.radius = -1  # Raises ValueError

Custom Decorators on Methods

When decorating a method, the wrapper must accept self as the first argument:

python
1import functools
2import time
3
4def timer(func):
5    @functools.wraps(func)
6    def wrapper(self, *args, **kwargs):
7        start = time.time()
8        result = func(self, *args, **kwargs)
9        elapsed = time.time() - start
10        print(f"{func.__name__} took {elapsed:.4f}s")
11        return result
12    return wrapper
13
14class DataProcessor:
15    @timer
16    def process(self, data):
17        time.sleep(0.1)  # Simulate work
18        return [x * 2 for x in data]
19
20p = DataProcessor()
21result = p.process([1, 2, 3])
22# process took 0.1003s

Generic Decorator (Works on Functions and Methods)

Using *args, **kwargs makes a decorator work on both standalone functions and methods:

python
1import functools
2
3def log_call(func):
4    @functools.wraps(func)
5    def wrapper(*args, **kwargs):
6        print(f"Calling {func.__name__}")
7        result = func(*args, **kwargs)
8        print(f"{func.__name__} returned {result}")
9        return result
10    return wrapper
11
12# Works on regular functions
13@log_call
14def add(a, b):
15    return a + b
16
17# Works on methods
18class Calculator:
19    @log_call
20    def multiply(self, a, b):
21        return a * b
22
23add(2, 3)
24# Calling add
25# add returned 5
26
27Calculator().multiply(4, 5)
28# Calling multiply
29# multiply returned 20

Decorator with Parameters

To pass arguments to a decorator, add an outer function:

python
1import functools
2
3def require_role(role):
4    def decorator(func):
5        @functools.wraps(func)
6        def wrapper(self, *args, **kwargs):
7            if not hasattr(self, 'user_role') or self.user_role != role:
8                raise PermissionError(f"Requires role: {role}")
9            return func(self, *args, **kwargs)
10        return wrapper
11    return decorator
12
13class AdminPanel:
14    def __init__(self, user_role):
15        self.user_role = user_role
16
17    @require_role('admin')
18    def delete_user(self, user_id):
19        return f"Deleted user {user_id}"
20
21    @require_role('admin')
22    def reset_system(self):
23        return "System reset"
24
25admin = AdminPanel('admin')
26print(admin.delete_user(42))  # Deleted user 42
27
28viewer = AdminPanel('viewer')
29# viewer.delete_user(42)  # Raises PermissionError

Class-Based Decorators

Implement a decorator as a class with __call__:

python
1class CountCalls:
2    def __init__(self, func):
3        functools.update_wrapper(self, func)
4        self.func = func
5        self.count = 0
6
7    def __call__(self, *args, **kwargs):
8        self.count += 1
9        print(f"{self.func.__name__} called {self.count} times")
10        return self.func(*args, **kwargs)
11
12@CountCalls
13def say_hello(name):
14    return f"Hello, {name}!"
15
16say_hello("Alice")  # say_hello called 1 times
17say_hello("Bob")    # say_hello called 2 times
18print(say_hello.count)  # 2

Class-Based Decorator on Methods

When used on methods, a class-based decorator needs the descriptor protocol (__get__):

python
1import functools
2from types import MethodType
3
4class Memoize:
5    def __init__(self, func):
6        functools.update_wrapper(self, func)
7        self.func = func
8        self.cache = {}
9
10    def __call__(self, *args, **kwargs):
11        key = (args, tuple(sorted(kwargs.items())))
12        if key not in self.cache:
13            self.cache[key] = self.func(*args, **kwargs)
14        return self.cache[key]
15
16    def __get__(self, obj, objtype=None):
17        if obj is None:
18            return self
19        return MethodType(self, obj)
20
21class MathService:
22    @Memoize
23    def fibonacci(self, n):
24        if n < 2:
25            return n
26        return self.fibonacci(n - 1) + self.fibonacci(n - 2)
27
28svc = MathService()
29print(svc.fibonacci(30))  # 832040 (computed once, cached)

Stacking Multiple Decorators

Decorators execute bottom-up (closest to the function runs first):

python
1@decorator_a
2@decorator_b
3def my_method(self):
4    pass
5
6# Equivalent to: my_method = decorator_a(decorator_b(my_method))

Common Pitfalls

  • Forgetting functools.wraps: Without @functools.wraps(func), the decorated function loses its original __name__, __doc__, and __module__. Always use functools.wraps in the wrapper function.
  • Not accepting self in method decorators: A decorator designed for standalone functions breaks on methods because self is passed as the first argument. Use *args, **kwargs to handle both cases.
  • Class-based decorators failing on methods: A class with __call__ does not work as a method decorator unless it implements __get__ (the descriptor protocol). Without it, self from the class instance is not passed correctly.
  • Decorator order matters with stacking: @auth @log def f() means auth(log(f)). The outermost decorator runs first on each call but wraps last during decoration. Order affects behavior when decorators interact.
  • Mutable default state in class decorators: A class-based decorator shares state across all calls. If decorating multiple methods, each gets the same decorator instance only if defined once. Use per-instance storage if isolation is needed.

Summary

  • Use @staticmethod, @classmethod, and @property for standard method behavior modifications
  • Custom method decorators must handle self — use *args, **kwargs for compatibility with both functions and methods
  • Always use @functools.wraps(func) to preserve the original function's metadata
  • Class-based decorators implement __call__ and need __get__ for method decoration
  • Decorators stack bottom-up: the decorator closest to the function wraps it first

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.