top of page

Decorators: Metadata, Retries and Logging

  • Writer: Mic
    Mic
  • Nov 27, 2025
  • 4 min read

In this second part of our deep dive into decorators, we’ll focus on two particularly practical patterns: Retries and Logging.

Unlike the more general-purpose cache decorators from part one, these decorators are designed to solve very specific problems. Retry decorators help make code more resilient to transient failures, while logging decorators provide structured visibility into how and when functions are executed. Together, they’re simple tools that can significantly improve robustness and observability in the development of applications.

Letting generative AI go wild with the prompt: Interpret python, decorator for metadata, retries and logging

Both of these decorators rely only on the core functools library—no extra dependencies, no magic. In the examples so far, though, we’ve quietly used @wraps(inner) without really stopping to explain why it’s there.

That omission is mostly fine for toy examples, but in real-world code it quickly turns into a production and debugging concern. If we define a decorator without @wraps, we start losing important metadata about the original function. To see what that looks like in practice, let’s strip it out and define our decorator like this…

Then things get a little weird when we try to inspect the metadata of the decorated function prototype.

Instead of seeing the original function’s name, docstring, or signature, we’re suddenly looking at the wrapper itself. From Python’s point of view, the original function has effectively been replaced, and all that useful metadata goes with it. This is where missing @wraps quietly turns from a harmless shortcut into a genuine debugging headache. In code

If we instead define the decorator with @wraps

then all the correct metadata gets transferred to the decorated function as we want it.


Metadata: If you really like structure

As we discussed above, function metadata isn’t preserved if we skip the @wraps decorator. That loss is usually accidental and annoying.

On the flip side, decorators can also be used deliberately to attach new metadata to a function. This can be surprisingly useful. For example, if we want to organise or group functions by category—say for documentation, discovery, or lightweight plugin systems—we can use a decorator to annotate them with that information. One simple way to do that looks like this…

Similarly, decorators don’t have to stop at adding brand-new metadata—they can also modify or extend metadata that’s already there.

Since much of a function’s metadata is generated automatically by Python, a decorator gives us a clean hook to tweak things like names, docstrings, or custom attributes in a controlled and explicit way. Used carefully, this can be a powerful technique for shaping how functions are discovered, documented, or introspected.

One practical example is marking a function as still being in the prototype stage.

By attaching this information as metadata, we can make that status explicit and machine-readable, rather than relying on comments or tribal knowledge. This makes it easier for tooling, tests, or even other developers to detect experimental code and treat it accordingly. This idea can, of course, be pushed a step further. By turning the metadata name itself into a parameter of the decorator, we can build a generic mechanism for attaching any metadata we care about.

With that approach, a single decorator can be reused to annotate functions with arbitrary attributes—feature flags, ownership, stability levels, or anything else your codebase needs—while keeping that information explicit, structured, and easy to inspect or append already existing metadata.


Retries: When once is not enough

Now let’s look at a more concrete use case. Suppose a function needs to access a database or make a server request. Occasionally, that call might fail due to a transient connection error—but failing outright isn’t always what we want. Often, retrying the operation a few times is enough to recover. This is a perfect fit for a retry decorator. By wrapping the function, we can centralise the retry logic and keep the function itself focused on what it actually does. A simple version of such a decorator might look like this…

This wrapper retries the wrapped function whenever one of the specified exceptions is raised. It keeps track of how many attempts are allowed and how much delay to insert between retries, backing off just enough to give transient failures a chance to resolve.

By pushing this logic into a decorator, the retry behaviour stays consistent and configurable, without cluttering the core function with control flow and error-handling boilerplate.

As a test case, we can have the simulated server call raise a ValueError instead of a ConnectionError, while configuring the decorator to only handle the ConnectionError. In that situation, no retry is attempted.

The exception is raised immediately, just as it would be without the decorator, which is exactly what we want—retrying is unlikely to fix anything and would only hide the real problem.


Logging: For simple logging without extra packages

Early-stage debugging often starts with the classic approach: sprinkling print statements everywhere to supervise what’s going on, and then ripping them all out again once things work.

For functions, this process can be cleaned up a bit by moving that noise into a dedicated decorator. With a simple logging decorator, we can trace function calls and behaviour without polluting the function body itself.

In this setup, every time the function Increase is called, a message is printed to standard output. That’s already useful, but we can push the idea a bit further.

Let’s add timestamps so we know when a call happened, and include error messages so failures don’t silently slip by. At the same time, we’ll make sure everything is written to standard error instead of standard output. That way, the log can be easily redirected into a file—for example by running Python with 2> log.txt—without interfering with normal program output.

Final thoughts

These are a few practical, more structural decorators that sit slightly outside the “nice to have” category. You can absolutely write perfectly good Python without ever touching them.

That said, once projects grow a bit larger, patterns like retries, logging, and metadata annotations can quietly save time and reduce boilerplate. They help keep core logic focused, while pushing cross-cutting concerns into reusable, well-contained pieces of code.

Next time, we’ll take a look at decorators for rate limiting—useful when you want to avoid hammering an API and getting yourself politely (or not so politely) blocked—as well as a closer look at context variables and how they fit into this picture.

Comments


bottom of page