Skip to content

The deferred namespace

 3 min A custom Python namespace records imports, then resolves deferred names on first lookup.
Published

Sep 11, 2017

I published a gist that makes imports lazy through two dunder methods.

The namespace is a dict subclass. Its __defer__(args, *names) records the original __import__ argument tuple for each name a deferred import would bind, without importing anything. Its __missing__ replays that tuple the first time anyone reads the name, then caches the result back into the namespace so the second read is an ordinary dict hit:

def __missing__(self, name):
    deferred = self.deferred[name]
    try: resolved = deferred.__import__()
    except KeyError as e: raise ImportError(...) from e
    else: self[name] = resolved
    finally: deferred.done = True
    return resolved

The failure translation is load-bearing. __missing__ is invoked by a failed lookup, so a KeyError escaping from inside it is indistinguishable from “name absent”—the resolution error would be swallowed into a plain NameError. Converting to ImportError keeps the real diagnosis. The adjacent done = True records completion: finally sets it only after deferred.__import__() returns or raises, and __missing__ never reads it. The hypothetical interpreter hook described in the gist could use that flag, but the namespace itself does not prevent reentry during resolution.

Installing it uses metaclass __prepare__, which returns the mapping used as the class-body namespace. The gist’s working form is class ModuleType(metaclass=MetaModuleType):; omitting the keyword lets __prepare__ use its defer=True default. Writing class M(metaclass=MetaModuleType, defer=True) is not equivalent here: the keyword continues into metaclass construction and raises TypeError unless __new__ or __init__ consumes it too. Inside the working class body, an unresolved name triggers the deferred import at first use, not at the import statement. There is even a small self-bootstrapping flourish: __defer__ = locals().__defer__ grabs a method off the namespace currently under construction.

CPython reads real module globals through a fast path that never calls __missing__, so the trick works for class bodies and exec namespaces but cannot make a module’s own top-level imports lazy. That is exactly why the gist’s header frames it as an interpreter-change proposal rather than a library: if __import__ called globals().__defer__(...) whenever the namespace defined it, from spam import eggs would become lazy module-wide with no new syntax.

It wasn’t the first pass at the idea, either. The concrete pressure came from xacto, my CLI-from-introspection tool. As I explained in a contemporary Python-ideas reply, xacto scanned every tool’s signature to generate argparse subcommands; doing all of the real imports first could make a plain ./mytool --help take a long time. It installed a sys.meta_path hook, executed the tool in a custom dict, discarded the placeholder import binding, and let dict.__missing__ perform the real import on first use.

A function whose __globals__ was that dict triggered __missing__ in Python 3.4, but the equivalent lookup ended in NameError in Python 2.7. xacto therefore preloaded a selected tool’s imports before calling into it; the expensive work was deferred until selection, not indefinitely through every function body. The gist is the idea distilled to its minimum and aimed at the language instead of an application. The later PEP 690 proposal also placed lazy-import machinery inside the interpreter.