Python ships with decorators that are available the moment the interpreter starts, without a single import statement. These are @property, @staticmethod, and @classmethod -- three built-in functions that live in global scope and are used inside class definitions to change how methods behave. Beyond these, the functools module in the standard library provides decorators like @wraps, @lru_cache, @cached_property, and @total_ordering that solve common problems with a single line. This article covers each one with complete, runnable code.
All decorators covered here are part of Python itself. The first three (@property, @staticmethod, @classmethod) are built-in functions that exist in the global namespace of every Python program. The remaining decorators require an import from functools, which is a module in the standard library and does not need to be installed. Together, these decorators handle the patterns that appear in virtually every Python codebase: controlled attribute access, alternative constructors, caching, comparison operators, and metadata preservation for custom decorators.
@property: Attribute-Like Access to Methods
The @property decorator turns a method into something that behaves like an attribute. When you access the property, Python calls the method behind the scenes without the caller needing parentheses. This lets you add validation, computation, or logging to attribute access without changing the interface:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius # routes through the setter for validation
@property
def celsius(self):
"""Get temperature in Celsius."""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Set temperature, rejecting values below absolute zero."""
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
"""Computed property: converts Celsius to Fahrenheit."""
return self._celsius * 9 / 5 + 32
temp = Temperature(100)
print(temp.celsius) # 100 (calls the getter, no parentheses)
print(temp.fahrenheit) # 212.0 (computed on access)
temp.celsius = 0 # calls the setter
print(temp.fahrenheit) # 32.0
try:
temp.celsius = -300 # setter rejects invalid value
except ValueError as e:
print(e) # Temperature cannot be below absolute zero
The @property decorator creates a getter. The @celsius.setter decorator creates a setter for the same property. The fahrenheit property has no setter, making it read-only. In a manual class without @property, you would need get_celsius() and set_celsius() methods, and every caller would need to use parentheses. The property decorator makes the interface cleaner while keeping the validation logic in place.
@property named value but no @value.setter. What happens when you try to assign to obj.value = 10?@staticmethod: Methods Without self
A regular method in a class automatically receives the instance as its first argument (self). The @staticmethod decorator removes that automatic argument, turning the method into a plain function that happens to live inside the class namespace:
class Validator:
@staticmethod
def is_valid_ip(address: str) -> bool:
"""Return True if address is a valid dotted-decimal IPv4 address."""
parts = address.split(".")
if len(parts) != 4:
return False
return all(
p.isdecimal() # digits only, no Unicode superscripts
and not (len(p) > 1 and p[0] == "0") # reject leading zeros
and 0 <= int(p) <= 255
for p in parts
)
@staticmethod
def is_valid_port(port: int) -> bool:
"""Return True if port is a valid TCP/UDP port number."""
return isinstance(port, int) and 1 <= port <= 65535
# Called on the class (no instance needed)
print(Validator.is_valid_ip("192.168.1.1")) # True
print(Validator.is_valid_ip("999.0.0.1")) # False
print(Validator.is_valid_ip("192.168.01.1")) # False (leading zero)
print(Validator.is_valid_port(443)) # True
# Also works on an instance
v = Validator()
print(v.is_valid_port(80)) # True
These methods do not access self or any instance data. They are utility functions grouped under the Validator class for organizational purposes. Without @staticmethod, Python would pass the instance as the first argument, which the method does not need or want.
@staticmethod method directly access?@classmethod: Methods That Receive the Class
The @classmethod decorator replaces self with cls, giving the method access to the class itself rather than a specific instance. The primary use case is alternative constructors -- methods that create new instances of the class from different types of input:
import json
class Config:
def __init__(self, host, port, debug=False):
self.host = host
self.port = port
self.debug = debug
@classmethod
def from_json(cls, json_string):
"""Create a Config from a JSON string."""
data = json.loads(json_string)
return cls(
host=data["host"],
port=data["port"],
debug=data.get("debug", False),
)
@classmethod
def from_dict(cls, data):
"""Create a Config from a dictionary."""
return cls(
host=data["host"],
port=data["port"],
debug=data.get("debug", False),
)
def __repr__(self):
return (f"Config(host={self.host!r}, port={self.port}, "
f"debug={self.debug})")
# Standard constructor
c1 = Config("localhost", 8080)
print(c1)
# Config(host='localhost', port=8080, debug=False)
# Alternative constructor from JSON
c2 = Config.from_json('{"host": "0.0.0.0", "port": 443, "debug": true}')
print(c2)
# Config(host='0.0.0.0', port=443, debug=True)
# Alternative constructor from dict
c3 = Config.from_dict({"host": "10.0.1.1", "port": 22})
print(c3)
# Config(host='10.0.1.1', port=22, debug=False)
The cls parameter in from_json and from_dict refers to the Config class. Calling cls(...) inside the method creates a new Config instance. This matters for inheritance: if a subclass inherits these methods, cls will refer to the subclass, and the alternative constructors will create subclass instances instead of Config instances.
The key distinction: @staticmethod has no access to the instance or the class. @classmethod has access to the class via cls but not to a specific instance. Regular methods have access to the instance via self and the class via type(self) or self.__class__. For a detailed side-by-side comparison, see @classmethod vs @staticmethod in Python.
@classmethod named from_string is defined on class Animal. A subclass Dog inherits it without overriding. When you call Dog.from_string("rex"), what does cls refer to inside the method?functools.wraps: Preserving Metadata
When you write a custom decorator, the wrapper function replaces the original function. Without intervention, the original function's __name__, __doc__, and __module__ are lost. @functools.wraps copies the original function's metadata onto the wrapper:
from functools import wraps
def log_call(func):
@wraps(func) # <-- preserves func's metadata
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_call
def connect(host, port):
"""Establish a connection to the given host and port."""
return f"Connected to {host}:{port}"
# Metadata is preserved
print(connect.__name__) # connect (not "wrapper")
print(connect.__doc__) # Establish a connection to the given host and port.
Without @wraps(func), the name would be "wrapper" and the docstring would be None. This is not just cosmetic -- frameworks like Flask use function names for URL routing, and documentation generators rely on docstrings. @wraps should be applied in every custom decorator. For a complete walkthrough of how to use it, see how to use functools.wraps inside a decorator.
functools.lru_cache and functools.cache: Memoization
@lru_cache caches the results of function calls. When the function is called again with the same arguments, the cached result is returned without re-executing the function body. This provides dramatic speedups for recursive or expensive computations:
from functools import lru_cache, cache
import time
# For recursive memoization, @cache (unbounded) is the correct choice.
# @lru_cache with a bounded maxsize risks evicting entries still needed
# by the active recursion stack.
@cache
def fibonacci(n: int) -> int:
"""Compute the nth Fibonacci number with memoization."""
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
start = time.perf_counter()
result = fibonacci(300)
elapsed = time.perf_counter() - start
print(f"fibonacci(300) = {result}")
print(f"Computed in {elapsed:.6f} seconds")
# fibonacci(300) = 222232244629420445529739893461909967206666939096499764990979600
# Computed in 0.000187 seconds
# Inspect cache performance
print(fibonacci.cache_info())
# CacheInfo(hits=298, misses=301, maxsize=None, currsize=301)
# @lru_cache with a bounded maxsize suits non-recursive look-ups
# where you want to cap memory use.
@lru_cache(maxsize=128)
def get_user(user_id: int) -> str:
"""Simulate an expensive user look-up."""
return f"User#{user_id}"
print(get_user(42)) # User#42
print(get_user(42)) # returned from cache
print(get_user.cache_info())
# CacheInfo(hits=1, misses=1, maxsize=128, currsize=1)
For recursive memoization, @cache is the correct tool: it caches every unique call without eviction, so no sub-problem is ever recomputed. Using @lru_cache with a bounded maxsize on a deeply recursive function risks evicting intermediate results that the active call stack still needs, which forces recomputation and defeats the purpose. @lru_cache(maxsize=N) is best suited to non-recursive look-up functions where you want to cap memory use. The cache_info() method shows hits (cached returns), misses (new computations), and current cache size. @cache was added in Python 3.9.
For recursive functions, always use @cache (unbounded) rather than @lru_cache(maxsize=N). A bounded cache can evict intermediate results that the active call stack still needs, causing recomputation. For non-recursive look-up functions where memory is a concern, use @lru_cache(maxsize=N). In both cases, arguments must be hashable — lists and dicts will raise TypeError. Avoid caching functions with side effects or time-dependent outputs like time.time().
@lru_cache(maxsize=4) and call it with five distinct argument values. What happens to the result cached for the first call?functools.total_ordering: Comparison Boilerplate
Implementing all six comparison methods (__eq__, __lt__, __le__, __gt__, __ge__, __ne__) on a class is tedious. The @total_ordering decorator lets you define just __eq__ and one other comparison method, and it generates the rest:
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major: int, minor: int, patch: int) -> None:
self.major = major
self.minor = minor
self.patch = patch
def _as_tuple(self) -> tuple[int, int, int]:
return (self.major, self.minor, self.patch)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return self._as_tuple() == other._as_tuple()
def __lt__(self, other: object) -> bool:
if not isinstance(other, Version):
return NotImplemented
return self._as_tuple() < other._as_tuple()
def __hash__(self) -> int:
# Define __hash__ whenever __eq__ is defined,
# so Version objects remain usable in sets and as dict keys.
return hash(self._as_tuple())
def __repr__(self) -> str:
return f"Version({self.major}.{self.minor}.{self.patch})"
v1 = Version(1, 0, 0)
v2 = Version(1, 2, 3)
v3 = Version(2, 0, 0)
# All comparison operators work, even though we only defined __eq__ and __lt__
print(v1 < v2) # True
print(v2 <= v2) # True
print(v3 > v2) # True
print(v1 >= v3) # False
print(v1 != v2) # True
# Sorting works because all comparisons are available
versions = [v3, v1, v2]
versions.sort()
print(versions) # [Version(1.0.0), Version(1.2.3), Version(2.0.0)]
# Version objects are hashable and can be used in sets
released = {v1, v3}
print(v2 in released) # False
Without @total_ordering, you would need to write all four additional comparison methods by hand. The decorator generates them from the two you provide, reducing five method definitions to two. One important note: Python sets __hash__ = None on any class that defines __eq__ without also defining __hash__, making instances of that class unhashable and unusable in sets or as dictionary keys. Always define __hash__ alongside __eq__.
Quick Reference Table
| Decorator | Import Required | Applies To | Purpose |
|---|---|---|---|
@property | None (built-in) | Methods | Attribute-like access with optional getter/setter/deleter |
@staticmethod | None (built-in) | Methods | Removes self, makes method callable without an instance |
@classmethod | None (built-in) | Methods | Receives cls instead of self, used for alternative constructors |
@functools.wraps | from functools import wraps | Wrapper functions | Preserves original function's __name__, __doc__, __module__ |
@functools.cache | from functools import cache | Functions | Unbounded memoization (Python 3.9+). Preferred for recursive functions — no eviction risk |
@functools.lru_cache | from functools import lru_cache | Functions | Bounded memoization; evicts least-recently-used when full. Use for non-recursive look-ups |
@functools.cached_property | from functools import cached_property | Methods | Like @property but computes once and stores on the instance; not thread-safe without a lock |
@functools.total_ordering | from functools import total_ordering | Classes | Generates missing comparison methods from __eq__ + one other. Always define __hash__ too |
from functools import wrapsfrom functools import cachefrom functools import lru_cachefrom functools import cached_propertyfrom functools import total_orderingHow To Apply These Decorators
The following steps summarize the practical application of each decorator covered in this article.
- Use
@propertyto control attribute access. Apply@propertyabove a method to make it behave like an attribute. Define a setter with@name.setterto add validation on assignment. Leave out the setter to make the property read-only. - Use
@staticmethodfor utility methods that need no instance. Apply@staticmethodabove any method that does not referenceselforcls. The method can then be called on the class directly, without creating an instance. - Use
@classmethodto create alternative constructors. Apply@classmethodabove a method and useclsas the first parameter instead ofself. Callcls(...)inside the method to create and return a new instance, enabling multiple construction paths for the same class. - Apply
@functools.wrapsinside every custom decorator. Importwrapsfromfunctoolsand apply@wraps(func)to the wrapper function inside your decorator. This copies__name__,__doc__, and__module__from the original function onto the wrapper so debugging tools and frameworks see the correct identity. - Use
@cacheor@lru_cacheto memoize functions. For recursive functions, use@cache(Python 3.9+, unbounded) — a boundedmaxsizecan evict results the active call stack still needs. For non-recursive look-up functions where memory is a concern, use@lru_cache(maxsize=N). Arguments must be hashable in both cases. Callcache_info()to inspect hit and miss counts. - Use
@total_orderingto reduce comparison method boilerplate. Importtotal_orderingfromfunctoolsand apply it to a class. Define__eq__and exactly one of__lt__,__le__,__gt__, or__ge__. The decorator generates the remaining four comparison methods automatically.
Frequently Asked Questions
- Which Python decorators are available without any imports?
Three decorators are available in Python's global scope without importing anything:
@property(turns a method into an attribute-like accessor),@staticmethod(removes the automaticselfparameter from a method), and@classmethod(replacesselfwithcls, giving the method access to the class instead of an instance).- What does
@propertydo in Python? @propertyturns a method into a descriptor that behaves like an attribute. When you accessobj.name, Python calls the getter method behind the scenes instead of returning a function object. You can also define.setterand.deletermethods on the same property to control assignment and deletion.- What is the difference between
@staticmethodand@classmethod? @staticmethodremoves bothselfandclsfrom the method signature. The method has no access to the instance or class and behaves like a plain function that happens to live inside the class namespace.@classmethodreplacesselfwithcls, giving the method access to the class itself. This makes@classmethoduseful for alternative constructors that need to create new instances of the class.- What does
@functools.wrapsdo? @functools.wrapsis a decorator applied to the wrapper function inside a custom decorator. It copies the original function's__name__,__doc__,__module__, and other metadata onto the wrapper, preserving function identity for debugging tools, documentation generators, and frameworks.- What does
@functools.lru_cachedo? @functools.lru_cachememoizes function results by caching up tomaxsizerecent calls. When the function is called with the same arguments again, the cached result is returned instead of recomputing. For recursive functions, use@cache(unbounded, Python 3.9+) instead — a bounded cache can evict intermediate results that the call stack still needs. Use@lru_cache(maxsize=N)for non-recursive look-up functions where you want to cap memory use.
Key Takeaways
- Three decorators require no import.
@property,@staticmethod, and@classmethodare built-in functions in Python's global scope. They are available in every Python program without importing anything. @propertyturns methods into attribute-like accessors. The getter runs on attribute access, the setter runs on assignment, and validation logic stays inside the class without changing the external interface.@staticmethodremoves self;@classmethodreplaces self with cls. Static methods are utility functions inside a class. Class methods receive the class and are primarily used for alternative constructors.@functools.wrapsbelongs in every custom decorator. It preserves the decorated function's name, docstring, and module, which frameworks and debugging tools depend on.@cacheand@lru_cacheprovide memoization for different use cases. Use@cache(Python 3.9+, unbounded) for recursive functions — it eliminates all recomputation without the risk of evicting results the call stack still needs. Use@lru_cache(maxsize=N)for non-recursive look-ups where capping memory use matters.@total_orderinggenerates comparison methods. Define__eq__and one of__lt__,__le__,__gt__, or__ge__, and the decorator fills in the remaining four comparison operators.
Python's built-in and standard library decorators solve the patterns that recur in every project: controlling attribute access, organizing class methods, caching expensive computations, preserving metadata in custom decorators, and reducing comparison boilerplate. They are production-tested, well-documented, and available without installing anything beyond Python itself. Knowing when to reach for each one is as important as knowing how to write your own.