The typing module is a heavy import. Alone, it adds over 45ms to import time. In trying to make cli tools and things that run in serverless (or any other on demand-scaling resource) environments faster, it’s one of the first libraries someone would want to ensure isn’t imported eagerly. In trying to also preserve runtime introspection so that library code is appropriate for use in any environment, this has caused some friction.
I would have expected that given something like the following:
# _typings.py
from __future__ import annotations
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Any, Final, Literal, Self, final
else:
def final(f): # noqa: ANN001
return f
def __getattr__(name: str):
if name in {"Any", "Final", "Literal", "Self"}:
import typing
return getattr(typing, name)
msg = f"module {__name__!r} has no attribute {name!r}"
raise AttributeError(msg)
__all__ = ["Any", "Final", "Literal", "Self", "final"]
from __future__ import annotations
from . import _typings as t # can't import by name and have lazy attr access
some_const: t.Final = ...
That static analysis tools would get the version from if TYPE_CHECKING
, and with the deferral of annotation evaluations, runtime introspection would only pay the import cost if the annotations are introspected.
In practice, this doesn’t work:
Ruff requires explicitly labeling this module as exporting typing equivalents: False positives when defering import of `typing.Literal` · Issue #15306 · astral-sh/ruff · GitHub
pyright intentionally requires Final be imported from typing and nowhere else: False positives when deffering import of typing special forms · Issue #9664 · microsoft/pyright · GitHub
It would be great if this “just worked” for users, but if tooling authors aren’t open to this (as it seems to be the case with pyright at least), if we could have a namespace in the standard library that had this behavior and could be special cased since apparently the special casing is preferred here, as to not be reimplemented by everyone needing it.