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.

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

What You'll Learn

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

✔ How real frameworks use advanced decorator patterns

✔ How to build your own production-ready decorator utilities

🔥 1. Decorators Refresher (1-Minute Summary)

🏠 Real-World Analogy:

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!

Step

What Happens

1. Define decorator

Create a function that takes another function as input

2. Create wrapper

Inside, define a wrapper function that adds behavior

3. Return wrapper

Return the wrapper (not call it!)

4. Apply with @

Use @decorator_name above a function

🧠 2. Why Decorators Matter in Real Projects

Every major Python ecosystem uses decorators for:

Understanding decorators = understanding real frameworks.

⚙️ 3. The Core Issue — Decorators Remove Metadata

⚠️ The Problem:

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!

Without @wraps

With @wraps ✅

func.__name__ → "wrapper"

func.__name__ → original name

func.__doc__ → None

func.__doc__ → original docstring

Debugger shows "wrapper"

Debugger shows real function name

🎯 4. Decorators That Accept Arguments (Decorator Factories)

🤔 The Challenge:

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 .

Layer

Purpose

Returns

Outer function

Accepts decorator arguments

Returns the actual decorator

Middle function

The actual decorator (takes fn)

Returns the wrapper

Inner function

The wrapper that runs

Calls the original fn

🔄 5. Stacking Multiple Decorators (Order Matters!)

🏠 Analogy: Layers of Wrapping Paper

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 .

Code Order

Execution Order

@A (top)

A runs FIRST (outermost layer)

@B (bottom)

B runs SECOND (inner layer)

def func:

Function runs LAST (the core)

⚡ 6. Example: Timing + Logging + Caching Stack

🧩 7. Real Project Example — Retry Decorator

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

🔒 8. Real Project Example — Input Validation Decorator

🧠 9. Passing Multiple Arguments to Decorators

🧵 10. Decorators With Keyword Arguments

🎓 11. Advanced Pattern — Class-Based Decorators

🤔 When to Use Classes?

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

Function Decorator

Class Decorator

Simple, one-off behavior

Needs state between calls

Uses closures for state

Uses self attributes

3 nested functions max

Cleaner for complex logic

🧨 12. Debugging Decorators (Common Problems)

These are the most common mistakes when writing decorators:

❌ Mistake

✅ Fix

Forgetting @wraps

Function name/docs disappear

Always add @wraps(fn)

Wrong decorator order

Unexpected behavior

Remember: bottom wraps first

Missing *args, **kwargs

Arguments don't pass through

Always use wrapper(*a, **k)

Forgetting to return result

Function returns None

Add return fn(*a, **k)

Calling instead of returning wrapper

Decorator runs immediately

Use return wrapper not return wrapper()

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

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

→ Modifying outer variable without nonlocal causes errors

🧪 13. Mini Project — Build a Full Decorator Suite

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

You now have a framework-level decorator system.

🎉 Conclusion

✔ How to create decorators that accept arguments

✔ How to design your own production-ready decorator system

📋 Quick Reference — Decorators

Syntax

What it does

🏆 Lesson Complete!

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.

Continue this course