Raymond Hettinger talked about module-level __getattr__ and __dir__ on Twitter/X and someone commented with a link to the module typing that defines a module-level __getattr__ as an excellent example of the usage of that dunder method to implement lazy loading of some names.
In the case of the module typing, the dunder method __getattr__ is used to lazily import some deprecated objects that the module typing exposes and that are expensive to create.
Since they're rarely needed, deferring their creation to a module-level __getattr__ makes importing the module typing much faster.
What I find particularly instructive about this example is that it shows how the module-level __getattr__ should behave:
AttributeError if that name isn't part of the module;globals()[attr] = obj; anddef __getattr__(attr):
"""Improve the import time of the typing module.
Soft-deprecated objects which are costly to create
are only created on-demand here.
"""
if attr == "ForwardRef":
obj = _lazy_annotationlib.ForwardRef
elif attr in {"Pattern", "Match"}:
import re
obj = _alias(getattr(re, attr), 1)
elif attr in {"ContextManager", "AsyncContextManager"}:
import contextlib
obj = _alias(getattr(contextlib, f"Abstract{attr}"), 2, name=attr, defaults=(bool | None,))
elif attr == "_collect_parameters":
import warnings
depr_message = (
"The private _collect_parameters function is deprecated and will be"
" removed in a future version of Python. Any use of private functions"
" is discouraged and may break in the future."
)
warnings.warn(depr_message, category=DeprecationWarning, stacklevel=2)
obj = _collect_type_parameters
else:
raise AttributeError(f"module {__name__!r} has no attribute {attr!r}")
globals()[attr] = obj
return obj