Python Cheatsheet

Decorators

Use this Python reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

What Decorators Are

A decorator is syntactic sugar for passing a function (or class) through another function and rebinding the name.

@decorator
def func():
    pass

# Exactly equivalent to:
def func():
    pass
func = decorator(func)

Basic Function Decorator

import functools

def my_decorator(func):
    @functools.wraps(func)   # preserves __name__, __doc__, __module__, etc.
    def wrapper(*args, **kwargs):
        print("before")
        result = func(*args, **kwargs)
        print("after")
        return result
    return wrapper

@my_decorator
def greet(name):
    """Say hello."""
    print(f"Hello, {name}!")

greet("Alice")
# before
# Hello, Alice!
# after

greet.__name__   # "greet"  (thanks to @wraps)
greet.__doc__    # "Say hello."

Decorator with Arguments

Add an outer layer to accept parameters.

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

@repeat(3)
def say(msg):
    print(msg)

say("hi")   # prints "hi" three times

Common Built-in Decorators

class MyClass:
    counter = 0

    @classmethod
    def create(cls):          # receives class as first arg
        return cls()

    @staticmethod
    def helper():             # no implicit first arg
        return 42

    @property
    def value(self):          # computed attribute
        return self._value

    @value.setter
    def value(self, v):
        self._value = v

    @value.deleter
    def value(self):
        del self._value

    def __init_subclass__(cls, **kwargs):   # called when subclassed
        super().__init_subclass__(**kwargs)

functools Decorators

from functools import lru_cache, cache, cached_property, wraps, total_ordering

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

fib.cache_info()    # CacheInfo(hits=..., misses=..., maxsize=128, currsize=...)
fib.cache_clear()

@cache              # unbounded memoization (Python 3.9+)
def expensive(x):
    ...

# Cached property — computed once, then stored as instance attribute
class DataLoader:
    @cached_property
    def data(self):           # computed only on first access
        return load_from_disk()

# total_ordering — fill in comparison methods automatically
@total_ordering
class Temperature:
    def __init__(self, degrees): self.degrees = degrees
    def __eq__(self, other): return self.degrees == other.degrees
    def __lt__(self, other): return self.degrees < other.degrees
    # __le__, __gt__, __ge__ are derived automatically

Timing and Debugging Decorators

import time
import functools

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

def debug(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        print(f"Calling {func.__name__}({', '.join(args_repr + kwargs_repr)})")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result!r}")
        return result
    return wrapper

Retry Decorator

import time
import functools

def retry(times=3, delay=1.0, exceptions=(Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == times - 1:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(times=5, delay=0.5, exceptions=(ConnectionError, TimeoutError))
def fetch(url):
    ...

Class-Based Decorators

Classes with __call__ can be decorators.

class Validate:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func

    def __call__(self, *args, **kwargs):
        if not args:
            raise ValueError("at least one argument required")
        return self.func(*args, **kwargs)

@Validate
def process(data):
    return data

# Class with arguments
class Retry:
    def __init__(self, times=3):
        self.times = times

    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(self.times):
                try:
                    return func(*args, **kwargs)
                except Exception:
                    pass
            raise RuntimeError("all retries failed")
        return wrapper

@Retry(times=5)
def flaky():
    ...

Stacking Decorators

Applied bottom-up (closest to the function first).

@decorator_a
@decorator_b
@decorator_c
def func():
    pass

# Equivalent to:
func = decorator_a(decorator_b(decorator_c(func)))

Decorator for Classes

# Decorating a class (not a method)
def add_repr(cls):
    def __repr__(self):
        attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
        return f"{cls.__name__}({attrs})"
    cls.__repr__ = __repr__
    return cls

@add_repr
class Config:
    def __init__(self, host, port):
        self.host = host
        self.port = port

Config("localhost", 8080)  # Config(host='localhost', port=8080)

# Singleton decorator
def singleton(cls):
    instances = {}
    @functools.wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Database:
    def __init__(self): ...

contextlib Decorator Helpers

from contextlib import contextmanager

@contextmanager
def timer_ctx():
    import time
    start = time.perf_counter()
    yield
    print(f"Elapsed: {time.perf_counter() - start:.4f}s")

with timer_ctx():
    expensive_operation()

Preserving Metadata (@wraps)

Without @wraps, the wrapper replaces metadata:

def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def original():
    """Original docstring."""
    pass

original.__name__   # "wrapper"  — WRONG
original.__doc__    # None       — WRONG

# Fix: always use @functools.wraps(func) on wrapper
def good_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper