How do you attach custom attributes to a function object, and when does a wrapper lose them?
answer
- A function is an object like any other
- The tag lives in an instance dict
- Wrappers start with an empty one
- functools copies it exactly once
- Decorator order decides who holds the tag
basics
~20 sA plain Python function has a dict, so a decorator can just assign fn.some_flag = value and later read it back. A wrapper that replaces the function loses those attributes unless functools.wraps merges the wrapped function's dict into it.
solid answer
~40 sFunctions are ordinary objects with an instance `__dict__`, so `fn.export_format = "csv"` works and shows up in `fn.__dict__`. Registration decorators use this constantly: the decorator tags the function and stores it in a module-level dict, and a dispatcher later reads the tag to choose behaviour. Attributes vanish in two situations. First, when a later decorator returns a **new** wrapper object without `functools.wraps` — the tag was set on the inner function, and the name now points at a wrapper whose `__dict__` is empty; `functools.WRAPPER_UPDATES` is `('__dict__',)`, which is precisely what fixes it. Second, when the tag is written *after* wrapping, since `wraps` copies the dict once at decoration time and does not keep the two in sync. Custom attributes are also invisible to static type checkers, so they are a runtime-only contract.
code
python · 26 linesFORMATTERS = {}
def formats(name):
def deco(fn):
fn.format_name = name
FORMATTERS[name] = fn
return fn
return deco
@formats("csv")
def to_csv(rows):
return "\n".join(",".join(r) for r in rows)
print(to_csv.__dict__, FORMATTERS["csv"].format_name)
def bare(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
@bare
@formats("tsv")
def to_tsv(rows):
return "\n".join("\t".join(r) for r in rows)
print(getattr(to_tsv, "format_name", "lost"))go deeper
Know that a function is an object and you can assign attributes to it, and that a decorator commonly does this to tag or register a function for later lookup.
Explain that the tag lives in the function's dict, that a wrapper starts with an empty one, and that functools.wraps merges the wrapped function's dict because WRAPPER_UPDATES is ('dict',).
Diagnose the loss in a real stack: read dict, spot a <locals> wrapper in qualname, follow wrapped, and reason about decorator ordering and about a registry holding a pre-wrap object.
Decide whether runtime tagging is the right contract at all. It is unchecked by tooling and fragile across wrappers, so weigh it against an explicit registration API or structured configuration before it spreads across a codebase.
### Functions have a __dict__ A function defined with `def` is an instance of a type that provides an instance dictionary, so arbitrary attributes can be assigned to it just like on any object. `fn.export_format = "csv"` stores the value in `fn.__dict__`, and `getattr(fn, "export_format", None)` reads it back. There is no special protocol involved; the dunder attributes such as `__name__` are stored in dedicated slots, while your custom keys live in the ordinary dict. The idiomatic use is the **tagging decorator**. In a nightly report generator, a `@formats("csv")` decorator sets `fn.format_name = "csv"`, records the function in a module-level `FORMATTERS` dict, and returns the same function unchanged. The dispatcher then picks a renderer by name, and any diagnostic can read `chosen.format_name` back off the object. Because the decorator returns the identical object, nothing about the function is disturbed — no wrapper, no metadata loss, no call overhead. ### The two ways the tag disappears **Wrapping without functools.wraps.** Decorators apply bottom-up. If `@bare_timer` sits above `@formats("tsv")`, the tag is written on the inner function, and then `bare_timer` returns a fresh wrapper. The module name is rebound to the wrapper, whose `__dict__` is empty, and `getattr(render, "format_name", "lost")` reports `lost`. The dispatcher still finds the *inner* function in `FORMATTERS`, because the registry captured it before wrapping — so calls bypass the timer entirely, which is a subtler bug than the missing attribute. Applying `functools.wraps(fn)` to the wrapper fixes the attribute half: `functools.WRAPPER_UPDATES` is `('__dict__',)`, so `update_wrapper` merges the wrapped function's dict into the wrapper's. **Writing the tag after the wrap.** `functools.wraps` copies the dict **once**, at decoration time. A tag assigned later to the inner function is not reflected on the wrapper, and a tag assigned to the wrapper is not visible on the inner function. The two dicts are independent after the copy. When ordering matters, put the tagging decorator outermost so it writes to the object the name actually ends up bound to. ### Other places the attribute is not where you expect A function accessed through an instance produces a bound method, and assigning an attribute to that raises `AttributeError` — the tag must go on the underlying function in the class body. Objects that merely *look* like functions have their own rules: a `functools.partial` object has no `__name__` and no function `__dict__` semantics you should rely on, and `functools.lru_cache` returns a distinct wrapper object that exposes `__wrapped__`. Builtin functions implemented in C reject attribute assignment outright. When a design must tag *any* callable, keep the mapping in an external dict keyed by the object rather than assuming attribute assignment succeeds. ### Attributes versus an external registry Both designs appear in real codebases and the tradeoff is worth stating. *Attribute on the function* keeps the metadata travelling with the object, survives being passed around, and is trivially readable in a debugger. It costs you: silent loss through wrappers, no static checking, and a namespace shared with anything else that decides to tag the same function — a collision is a silent overwrite, so prefix your keys. *External mapping* — a dict from function object to metadata — cannot be clobbered by a wrapper and can hold structured records, but it is one more thing to keep alive, it keys on identity so a wrapper is a different key, and it does not travel with the function across module boundaries. For a regression pack of 340 report cases, each declaring its expected output format through a decorator, the attribute form is the one that reads well at the call site; the discipline that keeps it correct is that every wrapper in the codebase uses `functools.wraps` and that the tagging decorator is applied outermost. If a locale-dependent format tag silently reverts to a default because one wrapper in the stack dropped the dict, the failure surfaces as wrong output rather than as an exception — which is why this belongs in a review checklist, not in tribal memory. ### Diagnosing it When a tag has gone missing, three reads settle it: `fn.__dict__` shows what the object actually carries; `fn.__qualname__` reading `something.<locals>.wrapper` proves a wrapper is standing in; and `fn.__wrapped__` walks one layer down, letting you compare the two dicts and see which layer holds the tag. ### The interview shape Say that functions have a `__dict__`, show the tagging decorator, name both loss modes, and name the fix — `functools.wraps` for the dict merge, plus decorator ordering — and be honest that this is a runtime-only contract no type checker will verify for you.
- Why can a registry hold the wrong object even when the custom attribute survives?Because the registration decorator captured the function it was handed, and an outer decorator then rebound the module name to a wrapper. The dict still points at the inner function, so dispatching through the registry calls it directly and skips every outer layer. Register outermost, or have the outer decorator re-register the object it returns.
- If a tag is assigned after decoration, does functools.wraps propagate it?No. `update_wrapper` copies the wrapped function's `__dict__` once, at decoration time; the two dicts are independent afterwards. A tag written later onto the inner function is invisible on the wrapper, and one written onto the wrapper is invisible on the inner function. Assign tags before wrapping, or write them to the outermost object.
- When would you keep the metadata in an external mapping instead of on the function?When the callables are not plain Python functions — builtins reject attribute assignment, and partial objects and C-implemented callables do not behave like a def — or when the metadata is structured and shared. An external dict cannot be dropped by a wrapper, but it keys on identity, so a wrapper is a different key and must be registered explicitly.
Tagging a function is a sticky note on a folder: fine until someone photocopies the folder into a new one and the note stays behind on the original.
saying these in an interview costs you the question
- Thinks you need a class to attach metadata to a callable
- Assumes attributes automatically follow through any decorator
- Believes functools.wraps keeps the two __dict__ objects in sync
- Ignores decorator ordering when reasoning about the tag
- Expects a static type checker to validate custom function attributes
- Assumes every callable accepts attribute assignment