bigonotes
CS ConceptsOOP

Decorators

How Python decorators wrap functions, and when to use @classmethod, @staticmethod, @property, @dataclass and your own.

4 min readLast updated OOP

A decorator is a function that takes a function and returns a new function, usually one that adds behavior before or after the original. Python ships several decorators that shape how class methods behave, and you can write your own for logging, timing, caching or retries.

What @ really does#

@decorator is syntactic sugar for reassigning the function:

@my_decorator
def say_hello():
    print("Hello!")

# is exactly the same as
def say_hello():
    print("Hello!")
say_hello = my_decorator(say_hello)

Method types in a class#

Regular methods#

Receive self (the object), so they can read instance and class data.

class Dog:
    def __init__(self, name):
        self.name = name

    def bark(self):
        print(f"{self.name} says Woof!")

@classmethod: alternative constructors#

Receives cls (the class), not an instance. Python doesn't support multiple __init__ overloads, so class methods are the idiomatic way to offer several ways to build an object.

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def from_string(cls, text):        # "Bruno-3"
        name, age = text.split("-")
        return cls(name, int(age))     # cls() also works for subclasses

    @classmethod
    def from_dict(cls, data):
        return cls(data["name"], data["age"])

d1 = Dog("Bruno", 3)
d2 = Dog.from_string("Max-5")
d3 = Dog.from_dict({"name": "Charlie", "age": 2})

Don't confuse this with __new__: __new__ controls whether and how memory is allocated (e.g. Singleton); @classmethod offers alternative ways to call the constructor.

@staticmethod: utilities that live with the class#

Receives neither self nor cls. It is a plain function grouped inside the class because it belongs with it conceptually.

class BankAccount:
    def __init__(self, balance):
        self.__balance = balance

    @staticmethod
    def validate_amount(amount):
        return amount > 0

    def deposit(self, amount):
        if self.validate_amount(amount):
            self.__balance += amount

print(BankAccount.validate_amount(100))  # True, no instance needed

@abstractmethod: enforce a contract#

Forces subclasses to implement the method; the abstract class itself can't be instantiated. See Abstraction.

from abc import ABC, abstractmethod

class Payment(ABC):
    @abstractmethod
    def pay(self, amount):
        pass

class CreditCard(Payment):
    def pay(self, amount):
        print(f"Paying {amount} via Credit Card")

CreditCard().pay(10)  # works; Payment() would raise TypeError

@property: attribute-style access#

Lets callers read (and optionally set) a value like an attribute while your code validates or computes it. See Encapsulation.

import math

class Circle:
    def __init__(self, radius):
        self.__radius = radius

    @property
    def radius(self):
        return self.__radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("radius must be non-negative")
        self.__radius = value

    @property
    def area(self):
        return math.pi * self.__radius ** 2

@dataclass: less boilerplate for data classes#

@dataclass generates __init__, __repr__ and __eq__ from type-annotated fields. Use it for classes that mainly hold data: API models, config objects, or LLD entities like ParkingSpot, Ticket, Seat.

from dataclasses import dataclass

@dataclass
class Dog:
    name: str
    age: int
    breed: str = "Unknown"   # default value

d1 = Dog("Bruno", 3, "Labrador")
d2 = Dog("Bruno", 3, "Labrador")
print(d1)        # Dog(name='Bruno', age=3, breed='Labrador')
print(d1 == d2)  # True

Type hints are not enforced at runtime#

def show(dog: Dog):
    print(dog)

show("None")  # runs fine: Python ignores the hint at runtime

Python is dynamically typed; hints help your IDE and tools, not the interpreter. You have two options:

def show(dog: Dog):
    if not isinstance(dog, Dog):  # explicit runtime check
        raise TypeError(f"Expected Dog, got {type(dog).__name__}")
    print(dog)

Or, more commonly, run a static type checker such as mypy that analyzes the code before it runs. Production Python teams typically combine type hints + mypy (catch data errors early) with abstract base classes (enforce the architecture).

Writing your own decorator#

A decorator is a function that takes a function and returns a wrapped version:

import functools

def logger(func):
    @functools.wraps(func)                 # keep func's name and docstring
    def wrapper(*args, **kwargs):          # accept any signature
        print(f"Calling {func.__name__}()")
        result = func(*args, **kwargs)
        print(f"Done: {func.__name__}()")
        return result                      # don't swallow the return value
    return wrapper

@logger
def add(a, b):
    return a + b

print(add(3, 5))
# Calling add()
# Done: add()
# 8

Common real-world decorators follow the same shape: @timer, @login_required, @retry, and functools.cache / functools.lru_cache for memoization.

Comparison#

The three method decorators differ in what they receive:

Decorator Receives Can access Use for
(none, regular method) self Instance + class Per-object behavior
@classmethod cls Class Alternative constructors
@staticmethod Nothing Neither Related utility functions
@abstractmethod self Instance + class Enforcing a contract
@property self Instance + class Validated or computed attributes
@dataclass The class n/a Generating data-class boilerplate
Custom The function Wraps it Adding cross-cutting behavior

Key takeaways#

  • @decorator means func = decorator(func).
  • @classmethod is Python's answer to constructor overloading; build with cls(...).
  • @staticmethod has no self or cls; it is a utility grouped with the class.
  • @dataclass removes boilerplate for data-holding classes.
  • Custom wrappers should take *args, **kwargs, return the result and use functools.wraps.
  • Type hints aren't enforced at runtime; use mypy or explicit checks.

Found a mistake? Tell me on LinkedIn ↗

Further reading#