S
Saurav Danej
90-Day AI/ML LinkedIn Content System
← All days
10
Day 10 of 90Python

Decorators — finally explained

POST 1 of 5 MorningPythonConcept

A decorator is just a function that returns a function

If there's one Python feature that consistently scares newcomers and confuses intermediates, it's the decorator. Take away the @ syntax and the fear evaporates.

@my_decorator
def foo(): ...

Is LITERALLY the same as:

foo = my_decorator(foo)

That's the entire concept. The @ is sugar for 'pass this function to the named callable, replace it with whatever the callable returns'.

my_decorator is a function. It receives foo (the function being decorated) as input. It returns something. Usually that 'something' is a wrapper function — another function that calls foo and adds some behaviour around it. The original 'foo' is now bound to the wrapper.

That's why the same decorator can:
- Time a function (wrap it, log how long it took)
- Cache results (wrap it, return cached value if available)
- Authenticate calls (wrap it, check user before running)
- Register a route (wrap it, add it to a routing table)
- Convert exceptions (wrap it, catch and re-raise)

All of these are 'function in, function out'. Just the wrapper does different things.

Once you internalise this — say it out loud once a day for a week if you have to — every framework you ever read becomes easier. @app.route('/users') in Flask: app.route is a function that returns a decorator that registers the wrapped function in app's routing table. @torch.no_grad(): torch.no_grad is a function that returns a decorator that disables gradient tracking around the wrapped function. Same pattern.

Decorators aren't magic. They aren't a special language feature. They're just function composition with prettier syntax. Internalise that, and Python's biggest source of mystery becomes one of its quieter pleasures.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#Decorators
POST 2 of 5 MiddayPythonDeep dive

Three decorator shapes you'll meet

Decorators come in three shapes. Knowing which is which makes reading framework code much easier.

Shape one — the plain decorator. @retry. The function gets wrapped directly. retry is a function that takes a function and returns a function.

Shape two — the parameterised decorator. @retry(times=3). retry(times=3) returns a decorator (which then takes the function and returns a wrapped function). Two layers of nesting. retry is now a 'decorator factory' — a function that produces decorators based on parameters.

This is the pattern that confuses intermediate Python users. You see @retry(times=3) and think 'a decorator with arguments'. It's actually 'a function call that returns a decorator, which then decorates'. The @ runs the call result, not the call itself.

Shape three — stacked decorators. Multiple @ lines on a single function. They apply bottom-up. @auth → @log → @cache → def foo() means: cache wraps foo, log wraps cache(foo), auth wraps log(cache(foo)). The outermost decorator runs first when the function is called.

Important — stacked decorators compose. The order matters. @auth above @log means auth runs before log on each call. Reverse them and authentication checks come AFTER logging, which usually isn't what you want.

A related must-know: functools.wraps. When your wrapper function has its own __name__ ('wrapper') and __doc__ (probably None), debugging gets confusing — stack traces show 'wrapper' instead of the original function name. @functools.wraps(fn) inside the decorator copies the original's metadata onto the wrapper. ALWAYS use it. Tomorrow's tip post is dedicated to it.

Three shapes. Same underlying mechanism. Once you can recognise them in the wild, decorator-heavy frameworks become readable.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonAdvanced
POST 3 of 5 AfternoonPythonCode

A timing decorator in 10 lines

Theory done. Here's a real, useful, drop-in decorator I use in every AI/ML repo I write.

@timed prints how long any function took to run. Drop it onto your training step, your retrieval call, your model load — anywhere you suspect a bottleneck. Fifteen seconds to add, lifelong utility.

Look at the structure. timed is a function that takes fn (the function being decorated). Inside, it defines wrapper, which records the start time, calls fn with whatever args it received, records the end time, prints the duration, returns the result. Then timed returns wrapper.

The @functools.wraps(fn) on the wrapper is non-negotiable. Without it, fn.__name__ becomes 'wrapper' inside the decorator, and your debug prints would say 'wrapper: 12 ms' instead of 'train_one_epoch: 12 ms'. Useless. With it, your debug output is meaningful and your stack traces still show the right function names.

The *args and **kwargs forwarding lets the decorator work on any function regardless of signature. The decorator doesn't care if the wrapped function takes 0 args or 27.

Use:

@timed
def train_one_epoch():
    ...

Now every call prints the timing. Nothing else changes — the function behaves identically.

For more sophisticated timing (collecting stats, sending to a metrics service, suppressing noisy logs), the structure expands but the shape doesn't change. function in, wrapper around it, function out.

This is the simplest decorator you'll write. The next 80% of decorators you'll write are variations on this template — caching, retrying, authenticating, logging. Same shape, different middle.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonCode
POST 4 of 5 EveningPythonTip

Always wrap your decorators in @functools.wraps

If you remember nothing else from today's posts, remember this — @functools.wraps is non-negotiable in any decorator you write.

Why is this so important? Because the wrapper function inside your decorator has its own identity. Without functools.wraps:

@my_decorator
def foo():
    """Does the foo thing."""
    pass

foo.__name__   # 'wrapper'  — wrong
foo.__doc__    # None       — wrong
foo.__module__ # decorator's module — wrong

This breaks more than aesthetics.

Flask routing breaks. Flask uses fn.__name__ to register endpoints. If two decorated functions both have __name__ == 'wrapper', Flask sees them as the same endpoint.

FastAPI breaks similarly. Auto-documentation pulls from __doc__ and __name__.

Debugger output is wrong. Stack traces show 'wrapper' on every line, no matter which actual function failed.

Logging is wrong. Anywhere you log fn.__name__ for diagnostics, you get 'wrapper' instead of the meaningful name.

Unit tests break. Anywhere a test imports a function by name and asserts its __name__, the assertion fails.

The fix is one decorator from the standard library:

import functools

def my_decorator(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper

functools.wraps copies fn's __name__, __doc__, __module__, __qualname__, and __wrapped__ attributes onto the wrapper. The wrapper now identifies itself as the wrapped function.

One line. No excuse. Add it to every decorator you write, forever. The day you forget is the day Flask routes start colliding silently.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonGotchas
POST 5 of 5 NightPythonRecap

Day 10 — decorators demystified

End of Day 10. Decorators were probably the topic I dreaded explaining most when I started planning this 90-day sprint, and they turned out to compress into four clean posts. Sometimes the scary topics aren't.

What we covered.

Morning, the foundational sentence — @ is just sugar for 'foo = decorator(foo)'. A decorator is a function that takes a function and returns a function. Once that lands, every framework you've ever found cryptic gets less cryptic.

Midday, the three decorator shapes you'll meet — plain, parameterised (decorator factory), and stacked. Knowing which shape a piece of code is using turns it from 'mysterious framework magic' into 'oh, that's a function returning a function'.

Afternoon, a real ten-line @timed decorator. The template you'll adapt for caching, retrying, logging, auth — same structure, different middle.

Evening, the one rule that's not optional — @functools.wraps. Without it, decorators silently break Flask routes, FastAPI docs, debuggers, and logs. With it, your decorator behaves correctly. One import, one line, no exceptions.

Some reflection. Decorators are the gateway to more advanced Python — context managers (Day 12), generators (Day 11), descriptors, metaclasses. They share a theme: Python lets you intercept normal mechanisms (function calls, with-statements, attribute access) and inject behaviour. The interception points are the language's deeper power.

Tomorrow, Day 11, we go to generators and lazy evaluation. The reason you can stream a 100GB file with a Python loop and not run out of memory. The pattern that makes data-loading pipelines, infinite sequences, and memory-bounded processing all work in plain Python.

See you in the morning.
#Python#AI#100DaysOfCode#BuildInPublic#PythonProgramming#PythonDecorators