← Back to Course Index
Lesson 8 of 10

Decorators

~20 min · first-class functions, closures, @decorator, @property, @wraps

Ref
Primary Source
Real Python — Primer on Python Decorators

The best in-depth guide on decorators. Read sections 1–4 for the essentials. Highly recommended primary source for this topic.

1 — First-Class Functions (the foundation)

Decorators are built on one simple fact: functions are objects in Python — just like in JavaScript. You already know this from JS.

JavaScript
// Functions are values
const greet = function(name) {
  return `Hello, ${name}`;
};

// Pass as argument
function applyTwice(fn, x) {
  return fn(fn(x));
}

// Return a function (factory)
function multiplier(n) {
  return (x) => x * n;
}
const double = multiplier(2);
double(5);  // 10
Python
def greet(name):
    return f"Hello, {name}"

# Functions are objects
print(type(greet))       # <class 'function'>
print(greet.__name__)    # "greet"

# Pass as argument
def apply_twice(fn, x):
    return fn(fn(x))

# Return a function (factory / closure)
def multiplier(n):
    def inner(x):
        return x * n
    return inner

double = multiplier(2)
double(5)   # 10

2 — What is a Decorator?

A decorator is a function that wraps another function to add behavior before or after it runs — without modifying the original. It's the same pattern as higher-order components in React or Express middleware.

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before the function")
        result = func(*args, **kwargs)   # call original
        print("After the function")
        return result
    return wrapper

# Manual application
def say_hello():
    print("Hello!")

say_hello = my_decorator(say_hello)   # wrap it
say_hello()
# Before the function
# Hello!
# After the function

# Syntactic sugar — the @ syntax does exactly the same
@my_decorator
def say_hello():
    print("Hello!")
💡 @ is just syntactic sugar

@my_decorator above a function is exactly equal to say_hello = my_decorator(say_hello) below it. The @ just makes it cleaner to read.

3 — Preserving Metadata with @wraps

Without @wraps, the decorated function loses its identity — its __name__, __doc__, etc. become those of the wrapper. Always use it.

from functools import wraps

def timer(func):
    @wraps(func)    # ← copy metadata from func → wrapper
    def wrapper(*args, **kwargs):
        import time
        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

@timer
def compute():
    """Expensive computation."""
    return sum(range(10_000_000))

print(compute.__name__)  # "compute" ✅ (not "wrapper")
print(compute.__doc__)   # "Expensive computation." ✅

4 — Decorators with Arguments

To pass arguments to a decorator, you need one more level of nesting — a factory that returns the decorator.

def repeat(n):
    """Decorator factory — returns a decorator"""
    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 greet(name):
    print(f"Hello, {name}!")

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

# This is equivalent to:
greet = repeat(3)(greet)

5 — Real-World Decorator Patterns

Logging

from functools import wraps

def log_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result!r}")
        return result
    return wrapper

Retry on failure

def retry(times=3, exceptions=(Exception,)):
    def decorator(func):
        @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
                    print(f"Attempt {attempt+1} failed: {e}. Retrying...")
        return wrapper
    return decorator

@retry(times=3, exceptions=(ConnectionError,))
def fetch_data(url):
    # might fail due to network issues
    ...

Caching (memoization)

from functools import lru_cache

@lru_cache(maxsize=128)
def expensive(n):
    return sum(range(n))

# Or manual cache
def memoize(func):
    cache = {}
    @wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

Access control

def require_auth(func):
    @wraps(func)
    def wrapper(request, *args, **kwargs):
        if not request.user.is_authenticated:
            raise PermissionError("Login required")
        return func(request, *args, **kwargs)
    return wrapper

# Common in web frameworks like Flask/Django
@require_auth
def dashboard(request):
    return render("dashboard.html")

6 — Built-in Decorators

class MyClass:

    @staticmethod
    def utility():
        """No self or cls — just a namespaced function"""
        return 42

    @classmethod
    def from_string(cls, s):
        """Gets cls (the class) instead of self"""
        return cls(int(s))

    @property
    def computed(self):
        """Called as obj.computed — not obj.computed()"""
        return self._val * 2

    @computed.setter
    def computed(self, value):
        self._val = value // 2
DecoratorWherePurpose
@staticmethodClassUtility with no class/instance access
@classmethodClassAlternative constructors; gets cls
@propertyClassComputed getter (called without ())
@lru_cacheAny functionMemoization with LRU eviction
@dataclassClassAuto-generate __init__, __repr__, __eq__

🧠 Quiz

1. What does @my_decorator above a function definition do?

2. Why should you use @wraps(func) inside a decorator?

3. How do you make a decorator that accepts its own arguments (like @repeat(3))?

4. @property lets you call a method as obj.value instead of obj.value(). True or false?

0/4

Questions answered correctly.