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#
@decoratormeansfunc = decorator(func).@classmethodis Python's answer to constructor overloading; build withcls(...).@staticmethodhas noselforcls; it is a utility grouped with the class.@dataclassremoves boilerplate for data-holding classes.- Custom wrappers should take
*args, **kwargs, return the result and usefunctools.wraps. - Type hints aren't enforced at runtime; use
mypyor explicit checks.