Skip to main content
Intermediate Python2026-07-29 · 10 min read

Python Decorators Explained: From @property to Retry Logic in 15 Minutes (2026)

Decorators are the feature that separates "knows Python" from "reads Python fluently". They look magical, but they're mechanical: @decorator on a function is exactly f = decorator(f). That's it. Everything else in this tutorial is that one line applied in different shapes.

The one-line theory

When you write:

▶ PYTHONeditable · runs in your browser

Python transforms it into:

▶ PYTHONeditable · runs in your browser

That's the entire mechanic. @decorator_name above a def runs def_name = decorator_name(def_name) after the definition. If you internalise this, decorators stop being mysterious forever.

Writing your first decorator (60 seconds)

A decorator is any callable that takes a function and returns a function. The typical shape:

▶ PYTHONeditable · runs in your browser

The *args, **kwargs in the wrapper is a defensive pattern — it lets the decorator work on any function regardless of signature. Skip it and you'll get TypeError: wrapper() takes 0 positional arguments but 2 were given the moment someone decorates a function with parameters.

The @functools.wraps fix

One subtle bug: after decorating, add.__name__ is "wrapper", not "add". Docstrings and type hints also vanish. The fix is one import:

▶ PYTHONeditable · runs in your browser

Rule: every decorator you write should use `@functools.wraps(fn)`. It costs one line and prevents an entire category of debugging pain (introspection, pytest fixtures, logging formatters, pickle — all break silently without it).

The four decorators you'll see in every codebase

1. @property — computed attributes

▶ PYTHONeditable · runs in your browser

@property makes a method callable as if it were an attribute. Perfect for read-only derived values. There's also @area.setter for the write path, but 90% of @property uses are read-only.

2. @staticmethod and @classmethod

▶ PYTHONeditable · runs in your browser

@classmethod gets cls (the class itself) and is used for alternative constructors. @staticmethod gets nothing special and is a plain function that just happens to live inside the class namespace.

3. @functools.cache — memoisation

▶ PYTHONeditable · runs in your browser

One of the most powerful stdlib decorators. Automatically memoises the function based on its arguments. Requires all args to be hashable (no lists / dicts as arguments — use tuples).

4. @dataclasses.dataclass — boilerplate elimination

▶ PYTHONeditable · runs in your browser

@dataclass reads the class-level type annotations and generates __init__, __repr__, __eq__, and (with frozen=True) __hash__ automatically. It's the single biggest quality-of-life improvement in Python 3.7+.

Writing a @retry decorator (real-world example)

A decorator that retries a function on failure with exponential backoff:

▶ PYTHONeditable · runs in your browser

Notice the three nested functions: retry accepts the config args and returns decorator; decorator accepts the function and returns wrapper; wrapper runs the retry loop. This three-layer pattern is what any decorator-with-arguments looks like — memorise the shape, not the specific example.

Class-based decorators (rare but useful)

A decorator is any callable. Classes with __call__ work too:

▶ PYTHONeditable · runs in your browser

Use when the decorator needs to hold state that outlives a single call (a counter, a cache, a rate-limit bucket).

Common pitfalls

1. Forgetting `@functools.wraps` — introspection breaks silently.

2. Mutable default arguments in the decorator itself — the classic def wrapper(x, cache=[]) bug bites here too.

3. Decorating methods without accepting `self` — remember self is just a positional argument; *args, **kwargs catches it fine.

4. Ordering matters when stacking: @a\n@b\ndef f() is f = a(b(f)). Read stacks bottom-up.

Practice

Write a @timing decorator that prints how long the wrapped function takes to run, then apply it to a function that computes sum(range(10_000_000)). The whole decorator is 8 lines with functools.wraps + time.perf_counter.


Once you internalise that @x is f = x(f), decorators become one of Python's most productive features. They're everywhere in real codebases: Flask/FastAPI routes (@app.route), pytest fixtures (@pytest.fixture), Django views (@login_required), Pydantic validators (@field_validator). Learning to write them yourself unlocks the ability to read every one of those.

Next step: 10 hands-on decorator lessons — build @retry, @memoise, @rate_limit, and @requires_auth from scratch with real Python running in your browser.

Ready to try it?

15 lessons free after a free signup — no card. Sample the platform, then decide.

Start free

Or unlock every lesson

Pro — $12/mo, unlimited AI, cancel any time. Refund within 14 days worldwide.

See Pro plans

Get one Python lesson + one career idea every Friday

No spam, no "buy our course now". Three bullets, every Friday. Unsubscribe with one click.