Decorators

Decorators are one of Python's most powerful features — used in Django, Flask, FastAPI, TensorFlow, PyTorch, logging systems, authentication, caching, and more. This lesson takes you far beyond the basics.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

✔ How to preserve metadata (the functools.wraps problem)

✔ How real frameworks use advanced decorator patterns

✔ How to build your own production-ready decorator utilities

Think of a decorator like gift wrapping . You have a present (your function), and the wrapper adds something extra (like a bow or ribbon) without changing what's inside. The wrapper can add behavior before or after opening the gift!

Every major Python ecosystem uses decorators for:

Understanding decorators = understanding real frameworks.

When you wrap a function, Python "forgets" the original function's name, docstring, and other info. This breaks documentation tools, debugging, and frameworks like FastAPI!

What if you want @check_role("admin") ? You need to pass an argument to the decorator! This requires an extra layer of nesting called a decorator factory .

Imagine wrapping a gift with multiple layers. The closest decorator to the function wraps first, then the next one wraps around that, and so on. When you call the function, you "unwrap" from outside in .

Used in API calls, database queries, and cloud services.

Use class-based decorators when you need to track state across multiple calls (like counting calls, caching results, or enforcing rate limits).

These are the most common mistakes when writing decorators:

→ Breaks documentation, tooling, introspection, FastAPI routes, pytest

→ Decorators run in unexpected order; auth might run after logging

→ Modifying outer variable without nonlocal causes errors

Build decorators for timing, logging, validation, caching, retries, and permissions:

You now have a framework-level decorator system.

✔ How to create decorators that accept arguments

✔ How to design your own production-ready decorator system

You can now write, stack, and configure decorators for any use case — the same pattern used by Flask, Django, and FastAPI.

Up next: Advanced Functions — master *args, **kwargs, and function signature tools.

Practice quiz

What does the @my_decorator syntax above a function actually do?

  • Calls the function immediately
  • Imports a module
  • Is shorthand for func = my_decorator(func)
  • Defines a class

Answer: Is shorthand for func = my_decorator(func). @my_decorator is shorthand for func = my_decorator(func) — it replaces func with the wrapper.

In a basic decorator, what should you do with the inner wrapper function?

  • Return it (not call it): return wrapper
  • Call it: return wrapper()
  • Delete it
  • Print it

Answer: Return it (not call it): return wrapper. You return the wrapper itself (return wrapper); calling it would run it immediately.

What does functools.wraps(fn) preserve?

  • The function's speed
  • The return value
  • The arguments
  • The original function's __name__ and __doc__ (its metadata)

Answer: The original function's __name__ and __doc__ (its metadata). @wraps copies metadata like __name__ and __doc__ from the original onto the wrapper.

Without @wraps, what does the decorated function's __name__ become?

  • The original name
  • 'wrapper'
  • None
  • 'decorator'

Answer: 'wrapper'. Without @wraps, the metadata is lost and __name__ shows 'wrapper'.

How many def statements does a decorator that accepts arguments (a decorator factory) typically have?

  • 3
  • 1
  • 2
  • 4

Answer: 3. A decorator with arguments has 3 levels: outer (takes args), middle (takes fn), inner wrapper.

With @decorator_A on top of @decorator_B, which wraps the function first?

  • A (the top one)
  • They wrap simultaneously
  • B (the bottom one, closest to the function)
  • Neither

Answer: B (the bottom one, closest to the function). The bottom decorator (closest to the function) wraps first; the top one runs first when called.

Why use *args, **kwargs in the wrapper signature?

  • To make it faster
  • So arguments pass through to the wrapped function correctly
  • To preserve metadata
  • It is required syntax for all functions

Answer: So arguments pass through to the wrapped function correctly. wrapper(*args, **kwargs) lets the wrapper accept and forward any arguments to fn.

For a class-based decorator, which method makes the instance usable as a decorator?

  • __init__
  • __main__
  • __wrap__
  • __call__

Answer: __call__. __call__ makes the instance callable, so it acts as the decorator; __init__ stores its arguments.

When is a class-based decorator most useful?

  • For simple one-off behavior
  • When you need to track state across multiple calls
  • When you want fewer lines
  • When the function has no arguments

Answer: When you need to track state across multiple calls. Class decorators use self attributes to track state (like call counts or rate limits) between calls.

In the validate_types decorator, what does add('5', 3) raise?

  • ValueError
  • Nothing, it returns '53'
  • TypeError
  • KeyError

Answer: TypeError. '5' is a str, not an int, so the isinstance check fails and raises TypeError.