skip to content

When does Python call a module-level `__getattr__` defined by PEP 562?

level: middleimportance: should knowfreq 30%

answer

  1. A fallback, never an interceptor
  2. It fires only on a lookup miss
  3. Module globals are searched first
  4. The hook must raise AttributeError itself
  5. PEP 562, Python 3.7

basics

~20 s

Only as a fallback. Attribute lookup on a module searches the module's globals first; when the name is missing there, Python calls the module's getattr with it. It must raise AttributeError for names it cannot supply.

solid answer

~50 s

PEP 562 (Python 3.7) lets a module define a top-level `def __getattr__(name)` - a plain function, no `self`. It is a **miss handler**, not an interceptor: `pkg.thing` first searches the module's `__dict__`, which *is* the module's global namespace, and only a failed lookup consults `__getattr__`. So anything bound at import time - functions, constants, an already-imported submodule, a value the hook itself cached - is returned directly and the hook never sees it, which is also why the mechanism costs nothing on the normal path. The hook must raise `AttributeError` for names it cannot provide; otherwise `hasattr()` is true for every name and code that probes a module for `__path__` or `__all__` gets nonsense back. `from pkg import thing` does route through it, `import pkg.sub` does not, and `dir(pkg)` will not list hook-provided names unless the module also defines `__dir__`.

code

python · 21 lines
python
import sys
import types

mod = types.ModuleType("demo")
mod.present = 1


def _module_getattr(name):
    if name == "computed":
        return 42
    raise AttributeError(f"module {mod.__name__!r} has no attribute {name!r}")


mod.__getattr__ = _module_getattr
sys.modules["demo"] = mod

import demo

print(demo.present)               # 1 - found in the module dict, hook never runs
print(demo.computed)              # 42 - supplied by the module-level __getattr__
print(hasattr(demo, "missing"))   # False - the hook raised AttributeError

go deeper

for a junior

Recall that a module is an object with attributes, and that mod.name normally just reads the module's global namespace. Knowing that a module can define a fallback for missing names is already ahead of the curve at this level.

for a middle

Be ready to explain the ordering precisely: dict lookup first, hook only on a miss, AttributeError if the hook cannot help. Expect to be asked why a name bound at import time never reaches the hook.

for a senior

Show that you know what the contract buys you in production: cheap package imports, a supported deprecation path, and the failure mode of a catch-all hook that makes hasattr answer true for __path__ and friends.

for a principal

Own the API-surface question: whether names should be resolvable dynamically at all, given that type checkers, documentation tools and IDEs cannot see them, and what __all__ plus a TYPE_CHECKING block must carry to keep the surface legible.

### The two hooks PEP 562, shipped in Python 3.7 and unchanged through 3.14, gave *module objects* two optional hooks: a top-level function `__getattr__(name)` and a top-level function `__dir__()`. Both are ordinary module-level functions written in a `.py` file - no class, no `self`. `__getattr__` receives the attribute name as a `str` and returns the value for it; `__dir__` takes no arguments and returns an iterable of names that `dir()` on the module will sort and show. ### Where it sits in the lookup path Every attribute access on a module object - `schedules.parse`, `getattr(schedules, "parse")`, and the attribute step of `from schedules import parse` - first searches the module's `__dict__`. That dict is not a private detail: it *is* the module's global namespace, the same namespace the module's own top-level code assigns into. Every function, class, constant and imported name created while the module executed lives there. Only when that search misses does CPython look up the key `__getattr__` in the very same dict and call it with the name. Two consequences follow directly, and both are what interviewers are probing: 1. **It cannot shadow anything.** If the module binds `parse = None` at import time, `mod.parse` finds `None` in the dict and the hook is never consulted. There is no module-level `__getattribute__`, so there is no way to intercept a name that already exists - the closest you get is not binding it in the first place, or `del`-ing the binding, which re-arms the hook. 2. **It is free on the hot path.** Ordinary attribute access pays nothing for the hook's existence; the function object is just another dict entry that is looked at only after a miss. ### The AttributeError contract A hook that cannot supply a name must raise `AttributeError`, ideally with the interpreter's own wording: ```python def __getattr__(name): if name in _LAZY: return _load(name) raise AttributeError(f"module {__name__!r} has no attribute {name!r}") ``` The classic bug is a catch-all hook that returns something for *every* name. `hasattr(mod, anything)` then answers `True`, and a surprising amount of machinery probes modules speculatively. A concrete, checkable example: `from plainmod import thing` against a non-package module first asks the module for `__path__` (to decide whether `thing` might be a submodule worth importing) before asking for `thing`. A catch-all hook answers that probe with garbage, and the import system now believes a plain module is a package. The failure surfaces far from its cause, which is exactly the kind of bug that costs an afternoon. ### What routes through it and what does not * `from pkg import thing` **does**. The from-import performs an attribute lookup on the already-imported module, and PEP 562 explicitly supports this path, which is what makes deprecation shims and lazy attributes usable by normal-looking import statements. * `import pkg.sub` **does not**. That is the finder-and-loader path; it goes through the import machinery and `sys.modules`, never through attribute access on `pkg`. * `dir(pkg)` **does not** show hook-provided names. `dir()` on a module defaults to the keys of its `__dict__`, and the hook contributes nothing to that dict until it binds something. This is why the two PEP 562 hooks are normally written as a pair: `__dir__` returns `sorted(set(globals()) | set(_LAZY))` so REPL completion and introspection still see the full public surface. * **Static tools do not see it at all.** A type checker, an IDE completion list or a documentation generator reads source, not runtime hooks. Keep `__all__` accurate and re-declare the real imports under `if TYPE_CHECKING:` so the names resolve statically while staying lazy at runtime. ### Why it exists Before 3.7 the only way to customize module attribute access was to build a subclass of `types.ModuleType` and assign an instance of it over `sys.modules[__name__]` at the bottom of the module. That worked, but it swapped out the object the import system had already handed to earlier importers, broke identity comparisons and reload, and confused pickling and introspection. PEP 562 replaced the whole trick with a supported hook that leaves the module object itself alone. ### What it is used for Three patterns dominate: keeping a **moved or renamed public name importable** while emitting a `DeprecationWarning`; **deferring an expensive submodule or object** until something actually touches it, so importing the package stays cheap; and computing a value that is genuinely expensive but rarely wanted. In every case the discipline is the same - handle a known set of names, bind or don't bind deliberately, and raise `AttributeError` for everything else.

  • Does `from pkg import thing` reach a module-level `__getattr__`?
    Yes. The from-import looks the name up as an attribute of the already-imported module, and PEP 562 hooks that path. Two details bite: against a package the hook is called twice for one statement (the fromlist handler probes first, then the bytecode fetches the name), and against a plain module the interpreter asks for `__path__` before asking for your name. So the hook must be cheap, idempotent, and must raise `AttributeError` for `__path__`.
  • How do you make `dir(pkg)` and REPL completion show a name the hook provides?
    Define a module-level `__dir__()` alongside it, taking no arguments and returning the names to show - typically `sorted(set(globals()) | set(_LAZY))`. `dir()` on a module otherwise reports only the keys of the module's `__dict__`, so lazily-provided names are invisible. It is also worth keeping `__all__` accurate, since that is what `from pkg import *` and most documentation tools read.
  • Why can't a module-level `__getattr__` override a name the module already defines?
    Because the dict lookup happens first and there is no module-level `__getattribute__` to intercept it. Modules only got the fallback hook, not the total-control one. If you need the hook to own a name, do not bind it at import time - or `del` the binding, which puts the name back into the miss path.

It is the receptionist you only reach after the directory board in the lobby has no entry for the name - if the board lists it, nobody ever asks the receptionist.

saying these in an interview costs you the question

  • Says it intercepts every module attribute access
  • Returns a placeholder for unknown names instead of raising AttributeError
  • Thinks it can shadow a name already bound in module globals
  • Confuses it with a class __getattr__ and expects a self parameter
  • Claims dir() automatically lists names the hook can supply
  • Believes from-imports bypass the hook entirely

context