✎ Edit content·DAY 019 · POST 4 OF 5 · Code Example

Decorators Demystified

Python · 12 slides
DAY 019 · POST 4 OF 5
(REMINDER)
DAY 019
Decorators You'll Actually Write
@saurav_dnj_24github.com/SauravDnj · linkedin.com/in/sauravdnj
1 / 12

Theme

Palette

Download

4K — sharpest, slowest
🎬 Video options
Preparing preview…
Live preview · loops the “none” effect
All rendering runs in your browser. No server, no cost, no upload. MP4/WebM = full motion + effects · GIF = lightweight loop · PNG/PDF = static for the Instagram & LinkedIn carousel.

Caption (tap to copy)

📖 Deep dive (full written explanation)

The slides stay clean and scannable. Here's the in-depth explanation behind each one — great for the blog version, show notes, or studying the topic properly.
Slide 1 · Decorators You'll Actually Write

This cover marks the transition from theory to practice. The previous three posts established what decorators are, why they matter, and how they're built; this one delivers decorators the reader will actually paste into real projects. The headline promises usefulness over cleverness — a timer, a cache, a retry, and the genuinely tricky parameterized case.

The instruction to read with a REPL open is deliberate. Decorators are best understood by running them and poking at them, and every snippet here is complete and runnable. The post is structured as a build-up: each example is slightly more advanced than the last, culminating in the three-layer decorator factory that takes its own arguments.

Slide 2 · 1. Timing decorator

The timing decorator is the simplest useful real-world example, and it follows the canonical skeleton exactly. It records a high-resolution start time with time.perf_counter, calls the wrapped function while forwarding all arguments, computes the elapsed time, prints it labeled with the function's name, and returns the original result untouched.

Two details are worth noticing. First, it uses perf_counter rather than time.time because perf_counter is monotonic and higher-resolution, the right tool for measuring durations. Second, func.__name__ prints correctly because of @functools.wraps — without it you'd see 'wrapper' in every timing line. This decorator is genuinely useful as-is for ad-hoc profiling, and it's the template the rest of the post varies.

Slide 3 · 2. Memoization cache

The memoization decorator introduces stateful decorators by caching results in a dict that lives in the closure scope. On each call it builds a key from the positional arguments, checks whether that key is already cached, computes and stores the result only on a miss, and returns the cached value. Applied to a naive recursive fibonacci, it turns exponential blowup into linear time.

The critical mechanic is where the cache lives: in the enclosing scope, captured by the closure, so it persists across calls and is shared by all invocations of that one wrapped function. This is the per-function shared state the mechanics post foreshadowed. Note the limitation flagged for later — using args as a dict key only works for hashable arguments, and this hand-rolled cache never evicts, which the mistakes post revisits.

Slide 4 · 3. Retry on failure

The retry decorator demonstrates a decorator that controls whether and how often the original runs, rather than just measuring it. It loops up to three times, returning immediately on success, logging and continuing on any exception, and re-raising after the final failed attempt. This is a common pattern for flaky network or I/O calls.

The important design choice is the final raise. The decorator doesn't silently return None when all attempts fail — it re-raises so the caller learns the operation genuinely failed. That choice connects directly to the 'silently swallowing errors' trap in the mistakes post. The hardcoded count of three is exactly the limitation that motivates the next example: making the retry count configurable requires a parameterized decorator.

Slide 5 · Retry flow

The decision diagram visualizes the retry decorator's control flow. The call to func is attempted; on success it returns the result immediately. On failure it checks whether attempts remain — if so, it logs the error and loops to try again; if not, it re-raises the exception. The two leaves, success-returns and exhausted-re-raises, are the only ways out.

Mapping the control flow visually clarifies the branching that the for-try-except code compresses. It also highlights the two exit points that matter for correctness: the happy path that returns a real value, and the failure path that surfaces the error rather than hiding it. Seeing the re-raise as a deliberate branch rather than an afterthought reinforces the error-handling discipline the series stresses.

Slide 6 · 4. A decorator with arguments

This is the post's centerpiece: a decorator that takes its own arguments, which requires three nested functions instead of two. The outermost function, retry, accepts the configuration (times). It returns decorator, the actual decorator that takes func. That returns wrapper, which runs the retry loop using the captured times value. The usage @retry(times=5) calls retry(5) first, getting back a decorator that then wraps flaky.

This three-layer structure is the single biggest conceptual jump in decorators, and it's where the desugaring habit pays off most. @retry(times=5) def flaky means flaky = retry(times=5)(flaky) — first call retry to get a decorator, then apply that decorator. Each layer has exactly one job: take the args, take the function, take the call. The next two slides break the three layers down further.

Slide 7 · Three layers, not two

This slide names the three layers explicitly so the nesting stops feeling arbitrary. The outer function exists to capture the configuration arguments. The middle function is the real decorator, the one that takes the function being decorated. The inner wrapper is what runs at call time. Three functions, three distinct jobs, each returning the next one inward.

The matryoshka framing helps: each layer wraps the next. The reason a parameterized decorator needs this extra layer is that the @ syntax only ever passes the function to whatever follows the @. So @retry(times=5) must make retry(times=5) evaluate to something that itself accepts a function — namely, a decorator. Understanding that the parentheses force an extra call is the key to never being confused by @deco() versus @deco again.

Slide 8 · The three-layer call

The pipeline diagram traces the three-layer call as a sequence of returns. retry(times=5) is called first and returns a decorator. That decorator is then called with flaky and returns a wrapper. Finally, whenever flaky is invoked, the wrapper runs and executes the retry loop. Each stage produces the input to the next.

Rendering it as a pipeline makes the staged evaluation concrete: configuration in, decorator out; function in, wrapper out; arguments in, result out. This is the same shape as the desugared expression retry(times=5)(flaky), just drawn left to right. Once a learner can map the parentheses in the source to these stages, parameterized decorators lose their mystery entirely.

Slide 9 · 5. The stdlib version

After hand-rolling memoization earlier, this slide shows the version you'd actually use in production: functools.lru_cache. It's a parameterized decorator from the standard library that caches results with a least-recently-used eviction policy, bounded by maxsize. It also exposes cache_info() for hit/miss statistics, which the hand-rolled version lacks.

The lesson is broader than caching: before writing a decorator, check whether the standard library already provides it. lru_cache, cached_property, singledispatch, and wraps itself are battle-tested and handle edge cases your quick version won't — like thread safety and bounded memory. Writing your own is valuable for learning and for genuinely custom needs, but reaching for the stdlib first is the mark of someone who knows the ecosystem.

Slide 10 · Patterns to remember

The tips slide consolidates the patterns demonstrated across the post into reusable rules. Always wrap with @functools.wraps to preserve identity. Use *args and **kwargs so the decorator stays general and works on methods. Put per-function state, like a cache, in the closure scope. When the decorator needs its own arguments, add the third layer. And check the standard library before building your own.

These five rules are essentially a recipe for writing correct, idiomatic decorators. They map directly to the examples just seen: wraps appeared in every one, *args made them general, the closure held the cache, the third layer enabled @retry(times=5), and lru_cache demonstrated the stdlib-first principle. Treat the list as a checklist whenever you write a new decorator.

Slide 11 · Forgetting the extra layer

The closing mistake targets the most common error with parameterized decorators: forgetting the third layer. A learner writes @retry(times=3) but defines retry as an ordinary two-layer decorator. Python evaluates retry(3) expecting it to return a decorator, but the two-layer version tries to treat 3 as the function to wrap, producing a confusing crash.

The diagnostic rule is simple and memorable: if the @ line has parentheses, the decorator needs three layers; if it doesn't, two layers suffice. Whenever you see @deco(args), mentally expand it to deco(args)(func) and confirm deco actually returns a decorator. This single check prevents the most frequent stumbling block when graduating from simple to parameterized decorators, and it sets up the dedicated parentheses-mix-up trap in the final post.

Slide 12 · Save this. Follow for Day 20.

This CTA closes the code-heavy post and points to the failure modes. Having written real decorators, the reader is now positioned to learn the subtle ways they go wrong — the bugs that don't show up as syntax errors but as confusing runtime behavior.

The teaser names three concrete traps: lost metadata, shared mutable state, and decorating methods. Framing the final post as a field guide to mistakes signals that it's about hardening the skills just acquired, turning someone who can write a decorator into someone who can write one that won't surprise them later.

🎨 AI image prompt (matches this theme + palette)

Paste into Midjourney, DALL·E, Ideogram, etc. to generate an on-brand image, then upload it on the Edit content page. The prompt updates automatically with the selected theme + palette.