skip to content

When does Python call a class's `__getattr__` method on an instance?

level: juniorimportance: should knowfreq 40%

answer

  1. It is a fallback, not an interceptor
  2. Runs only after normal lookup fails
  3. Triggered by AttributeError, receives the name
  4. Must raise AttributeError for unknown names
  5. Lives on the class, not the instance

basics

~20 s

Python calls getattr only as a fallback, after the normal attribute search of the instance dictionary and the class hierarchy has already failed. It receives the attribute name as a string and should raise AttributeError for names it cannot supply.

solid answer

~40 s

`__getattr__` is the *miss* hook. Evaluating `obj.name` runs `type(obj).__getattribute__`, which does the whole ordinary search; only if that raises `AttributeError` does the runtime fall back to `type(obj).__getattr__(obj, "name")`. So a name that already exists on the instance or the class never reaches it — which is exactly why it is cheap and why lazy attributes and record-like wrappers use it. It must be defined on the class (an instance attribute of that name is ignored for the implicit call), it receives the name as a `str`, and it must `raise AttributeError` for names it does not handle. Returning `None` instead makes `hasattr` always true, hides typos, and misleads every library that probes an object for an optional hook with `getattr`.

code

python · 12 lines
python
class Config:
    def __init__(self):
        self.host = "localhost"

    def __getattr__(self, name):
        return f"<default:{name}>"


c = Config()
print(c.host)      # localhost - found normally, __getattr__ never runs
print(c.missing)   # <default:missing>
print("host" in c.__dict__, "missing" in c.__dict__)  # True False

go deeper

for a junior

Be ready to state the one-line rule: the hook runs only after the normal attribute search has failed, and it must raise AttributeError for names it cannot supply. Know that it is defined on the class.

for a middle

Explain the mechanics: the dot operator calls getattribute first, and any AttributeError escaping it — including one raised inside a property — triggers the fallback. Show the AttributeError contract that hasattr and getattr-with-default depend on.

for a senior

Demonstrate judgement about where the hook belongs in real code: narrow name handling, translating a KeyError with from None, and the consequence of caching a resolved value into the instance dictionary, which permanently shadows the hook for that name.

for a principal

Own the API question: dynamic attributes are cheap to write and expensive to review, since they defeat static checking, editor completion and grep. Decide when explicit properties or a generated class are the better contract for a team.

## The two-stage protocol Attribute access on an instance is a two-stage affair. Writing `obj.name` compiles to a lookup that calls `type(obj).__getattribute__(obj, "name")`. That method is the one that does real work: it performs the ordinary search across the type and the instance and returns a value. Only if it finishes by raising `AttributeError` does the interpreter take the second stage — it looks for `__getattr__` **on the type**, and if the class (or one of its bases) defines it, calls `type(obj).__getattr__(obj, "name")` and returns whatever that produces. That ordering is the entire behaviour worth memorising: **`__getattr__` is a fallback, not an interceptor.** It can never see an access that succeeded. If `sku` is already in the instance `__dict__` or on the class, a `__getattr__` on that class simply does not run for `sku`. New readers often expect it to be a hook on *every* access; that is `__getattribute__`, a different and far more invasive method. ## The AttributeError contract `__getattr__` has one obligation: for a name it cannot supply, it must raise `AttributeError`. A large amount of Python leans on that exception as a *question*, not as a failure: * `hasattr(obj, "x")` is defined as "call `getattr` and see whether `AttributeError` came out". A `__getattr__` that returns `None` for everything makes `hasattr` return `True` for every name in the universe. * `getattr(obj, "x", default)` returns the default only when `AttributeError` escapes. * Serialisation and copying helpers such as `copy.deepcopy` and the `pickle` machinery probe objects for optional hook attributes with `getattr`. If your object cheerfully answers every probe with a non-callable value, those libraries take the wrong branch and fail in a place far from the cause. Raise it with the attribute name as the message, and use `raise AttributeError(name) from None` when you are translating a `KeyError` from a backing dictionary, so the traceback shows the real story. ## What counts as "lookup failed" The trigger is *any* `AttributeError` escaping the normal lookup — not only a missing name. If a `property` getter on the class raises `AttributeError` from deep inside its own body, the runtime sees `AttributeError` and dutifully calls `__getattr__`. On a class with a permissive `__getattr__`, the bug in the getter disappears and the caller silently gets the fallback value. This is one of the most confusing failure modes in Python, and it is a good reason to keep `__getattr__` narrow: handle the names you own, and re-raise for everything else. ## Where it must live, and what it is not The implicit call is looked up on the type, so `__getattr__` has to be a class attribute. Assigning `obj.__getattr__ = some_function` on an instance does not install the hook. It is also unrelated to the builtin `getattr()`, which is just the function form of the dot operator; the dunder is the hook the dot operator falls back to. `__slots__` does not disable it. An unset slot raises `AttributeError` on read, so `__getattr__` still fires — which makes `__slots__` plus `__getattr__` a compact way to build objects with no per-instance dictionary and a computed fallback. ## Cost, and the caching pattern Because the hook sits on the failure path, normal attribute reads pay nothing for its existence. The hook itself is comparatively slow: it is a Python-level call that runs only after the fast search has already been done and thrown. That is why the classic lazy-attribute idiom computes the value once inside `__getattr__` and then writes it into the instance `__dict__`: from that moment the name is found by the ordinary search and the hook is never entered again for it. The cost of that trick is that the value is now frozen — refreshing it means deleting the cached attribute — and that the instance grows for every name it has ever been asked for. ## Where you actually see it Record-like objects backed by a mapping, thin forwarding wrappers, lazily loaded configuration and "virtual" attributes computed from a schema all use it. Modules have the same facility: since 3.7, a module-level `__getattr__` function is consulted when an attribute is missing from the module, which is how a package emits deprecation warnings for a name on access instead of at import time. ## The review cost The reason experienced reviewers push back on `__getattr__` is not that it is slow or fragile — it is that the names it serves exist nowhere a reader or a tool can see them. A static type checker cannot verify `record.qty`, an editor cannot complete it, and grepping for the attribute finds nothing. Use it where the name set is genuinely dynamic (a row from an external feed, a forwarded namespace); prefer explicit attributes, a `property` or a generated class where the fields are known in advance, and implement `__dir__` when you do use it so introspection still shows something useful.

  • What breaks if `__getattr__` returns a value for every name it is asked?
    `hasattr` becomes permanently true and `getattr` with a default never returns the default, so typos stop failing. Worse, libraries that probe an object for optional hooks with `getattr` — `copy.deepcopy` and the pickling machinery among them — receive a non-callable answer and take the wrong branch, producing errors far from the real cause. Handle only the names you own and re-raise `AttributeError` for the rest.
  • Does defining `__slots__` stop `__getattr__` from firing?
    No. A slot that has never been assigned raises `AttributeError` on read, and a name that is not a slot at all raises it too, so the fallback still runs. The combination is actually useful: `__slots__` removes the per-instance dictionary while `__getattr__` supplies computed or forwarded names, and it also prevents anything from silently caching values into the instance.
  • If a `property` getter raises `AttributeError`, what does the caller see?
    The exception escapes the normal lookup, which is indistinguishable from the name being missing, so the runtime calls `__getattr__`. On a class with a permissive fallback, a genuine bug inside the getter is silently replaced by the fallback value. Keep `__getattr__` narrow, and inside a property convert unexpected `AttributeError`s into a different exception type so they cannot be swallowed.

It is a receptionist who is only called when a visitor's name is not on the guest list: everyone already listed walks straight past the desk.

saying these in an interview costs you the question

  • Says __getattr__ runs on every attribute access
  • Returns None for unknown names instead of raising AttributeError
  • Sets __getattr__ on the instance and expects it to fire
  • Thinks it can intercept a name already in the instance dict
  • Confuses the __getattr__ hook with the getattr() builtin

context