Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Cheat Sheets
  3. Python
  4. Python Decorators Cheat Sheet
PythonCheat Sheet

Python Decorators Cheat Sheet

Quick-reference for every decorator pattern you actually use. Each section includes copy-ready snippets with inline output comments.

On this page
  1. 1Decorator Basics
  2. 2@functools.wraps: Preserve Metadata
  3. 3Decorator with Arguments (Factory)
  4. 4Stacking Decorators
  5. 5Built-in Decorators: property, classmethod, staticmethod
  6. 6Caching Decorators: @lru_cache, @cache
  7. 7Class Decorators
  8. 8Practical Patterns: timer, retry, validate
  9. 9Decorating Async Functions
Decorator Basics@functools.wraps: Preserve MetadataDecorator with Arguments (Factory)Stacking DecoratorsBuilt-in Decorators: property, classmethod, staticmethodCaching Decorators: @lru_cache, @cacheClass DecoratorsPractical Patterns: timer, retry, validateDecorating Async Functions

Decorator Basics

@decorator above def f() is just f = decorator(f). A decorator receives a function and returns a (usually wrapped) function.

The mental model
@my_decorator
def greet():
    print('Hello!')

# Exactly equivalent to:
# greet = my_decorator(greet)
Simple decorator template
from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f'Calling {func.__name__}')
        result = func(*args, **kwargs)
        print(f'{func.__name__} returned {result}')
        return result
    return wrapper
Using the decorator
@my_decorator
def add(a, b):
    return a + b

add(2, 3)
# Calling add
# add returned 5
# => 5

@functools.wraps: Preserve Metadata

Without @wraps, the wrapper replaces the original function's __name__, __doc__, and __signature__.

Without @wraps (broken metadata)
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def greet():
    """Say hello."""
    pass

greet.__name__   # => 'wrapper'  (wrong!)
greet.__doc__    # => None       (lost!)
With @wraps (metadata preserved)
from functools import wraps

def good_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@good_decorator
def greet():
    """Say hello."""
    pass

greet.__name__   # => 'greet'       (correct!)
greet.__doc__    # => 'Say hello.'  (preserved!)

Decorator with Arguments (Factory)

Three layers: factory(args) returns decorator(func) returns wrapper(*args, **kwargs).

Repeat decorator factory
from functools import wraps

def repeat(n):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def say_hello():
    print('Hello!')

say_hello()   # prints 'Hello!' three times
Rate limiter factory
import time
from functools import wraps

def rate_limit(max_per_second):
    min_interval = 1.0 / max_per_second
    def decorator(func):
        last_call = [0.0]
        @wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.monotonic() - last_call[0]
            if elapsed < min_interval:
                time.sleep(min_interval - elapsed)
            last_call[0] = time.monotonic()
            return func(*args, **kwargs)
        return wrapper
    return decorator

Stacking Decorators

Bottom decorator wraps first. @g above @f means func = g(f(original)). When called, the outer wrapper runs first.

Stacking order
@auth_required    # 2. wraps the result of log_calls(fn)
@log_calls        # 1. wraps the original function first
def delete_user(user_id):
    ...

# Equivalent to:
# delete_user = auth_required(log_calls(delete_user))
Execution order at call time
from functools import wraps

def outer(func):
    @wraps(func)
    def wrapper(*a, **kw):
        print('outer before')
        result = func(*a, **kw)
        print('outer after')
        return result
    return wrapper

def inner(func):
    @wraps(func)
    def wrapper(*a, **kw):
        print('inner before')
        result = func(*a, **kw)
        print('inner after')
        return result
    return wrapper

@outer
@inner
def hello():
    print('hello')

hello()
# outer before
# inner before
# hello
# inner after
# outer after

Built-in Decorators: property, classmethod, staticmethod

@property (read-only computed attribute)
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def area(self):
        return 3.14159 * self._radius ** 2

c = Circle(5)
c.area   # => 78.53975  (no parentheses needed)
@property with setter
class Circle:
    def __init__(self, radius):
        self.radius = radius   # uses the setter

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

    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError('Radius must be positive')
        self._radius = value
@classmethod (alternative constructor)
class Date:
    def __init__(self, year, month, day):
        self.year, self.month, self.day = year, month, day

    @classmethod
    def from_string(cls, s):
        y, m, d = map(int, s.split('-'))
        return cls(y, m, d)

d = Date.from_string('2026-02-22')
d.year   # => 2026
@staticmethod (no self or cls)
class MathUtils:
    @staticmethod
    def is_even(n):
        return n % 2 == 0

MathUtils.is_even(4)   # => True

@staticmethod receives no implicit first argument. It is just a namespaced function.

Caching Decorators: @lru_cache, @cache

Memoize expensive function calls. Arguments must be hashable.

@lru_cache (bounded cache)
from functools import lru_cache

@lru_cache(maxsize=128)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

fib(100)   # => 354224848179261915075
fib.cache_info()
# => CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
@cache (unbounded, Python 3.9+)
from functools import cache

@cache
def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)

factorial(10)   # => 3628800

@cache is shorthand for @lru_cache(maxsize=None). Memory grows without bound.

Clear the cache
fib.cache_clear()   # Resets all cached results
fib.cache_info()    # => CacheInfo(hits=0, misses=0, ...)

Class Decorators

Decorators can also wrap classes. The decorator receives the class and returns a (possibly modified) class.

@dataclass (auto-generate boilerplate)
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
repr(p)          # => "Point(x=1.0, y=2.0)"
p == Point(1, 2)  # => True  (__eq__ auto-generated)
Custom class decorator (registry)
registry = {}

def register(cls):
    registry[cls.__name__] = cls
    return cls

@register
class MyPlugin:
    pass

registry   # => {'MyPlugin': <class 'MyPlugin'>}
@dataclass with options
from dataclasses import dataclass

@dataclass(frozen=True, slots=True)
class Config:
    host: str
    port: int = 8080

c = Config('localhost')
# c.host = 'x'  # FrozenInstanceError (immutable)

Practical Patterns: timer, retry, validate

@timer (measure execution time)
from functools import wraps
from time import perf_counter

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            elapsed = perf_counter() - start
            print(f'{func.__name__} took {elapsed:.4f}s')
    return wrapper
@retry (with backoff)
from functools import wraps
import time

def retry(max_attempts=3, backoff=2):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception:
                    if attempt == max_attempts - 1:
                        raise
                    time.sleep(backoff ** attempt)
        return wrapper
    return decorator

@retry(max_attempts=3, backoff=2)
def fetch_data(url):
    ...
@validate_args (type check)
from functools import wraps

def validate_types(**expected):
    def decorator(func):
        @wraps(func)
        def wrapper(**kwargs):
            for name, type_ in expected.items():
                if name in kwargs and not isinstance(kwargs[name], type_):
                    raise TypeError(f'{name} must be {type_.__name__}')
            return func(**kwargs)
        return wrapper
    return decorator

Decorating Async Functions

Async function decorators must use async def wrapper and await the original function.

Async timer decorator
from functools import wraps
from time import perf_counter

def async_timer(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        start = perf_counter()
        try:
            return await func(*args, **kwargs)
        finally:
            print(f'{func.__name__} took {perf_counter() - start:.4f}s')
    return wrapper
Sync wrapper on async = broken
# BROKEN: sync wrapper returns coroutine, never awaits it
def broken(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)   # returns coroutine object!
    return wrapper

A sync wrapper on an async function does not execute the function. Always use async def + await.

Learn Python in Depth
Python OOP Practice →Python Comprehensions Practice →
Warm-up1 / 2

Can you write this from memory?

Write a function `get_item()` (no params, body is `pass`) with the `@login_required` decorator applied

See Also
OOP →Error Handling →

Start Practicing Python

Free daily exercises with spaced repetition. No credit card required.

← Back to Python Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.