Static analysis at odds with runtime goals

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.

3 Likes

The only thing that stops this working with if TYPE_CHECKING is the fact that you have to import TYPE_CHECKING itself from typing. Maybe just that can be changed.

2 Likes

That’s not accurate, There’s design of tools causing other issues in that they special case certain symbols. Please see the related issues, and keep in mind that the intent here is that at runtime these are imported, but only if needed, and without having to duplicate this trick in every file that needs it.

Yes, this is a known (and as-designed) limitation of pyright. The Final symbol must be imported directly from typing and not re-exported from other modules […]

These tools also do understand the pattern:

TYPE_CHECKING = False
if TYPE_CHECKING:
    ...

this isn’t causing problems here.

Okay, I didn’t realise that. So it is already possible to do this now if you do the above in every module but your point is that it would be better not to have that boilerplate everywhere.

It seems a bit unfortunate that from __future__ import annotations doesn’t do this automatically somehow since it is obviously related.

It doesn’t work with existing tooling.

What I’m asking for is an agreement on any of the following, or anything else that would meaningfully improve how impactful this actually is to startup:

  • that it’s specified as part of the type system that tooling should respect re-exports of typing symbols for typing purposes (This is the part that doesn’t work right now, and I expect pushback to this.)
  • that typing gain this behavior, and all the definitions in typing get moved to _typing or similar.
  • That there’s an alternative standard namespace that has this behavior for typing symbols.

The only option that works currently is stub files, but this kills inline annotations for typing your own code and using it to assist with maintaining, and also kills any runtime use of the annotations that might not be hit in all uses of the library.

1 Like

off-topic: your dummy definition of final should probably set __final__ if the goal is to preserve runtime introspection.

on-topic:

This option seems least disruptive, but it would also mean that at the earliest, this is available in python 3.14. It’s a shame that some existing tooling authors consider this intentional and not worth improving, because the ideal would be that tooling just naturally does this without having to be pressured by specification.

Funny enough, this is the second time today someone has pointed out __final__ in relation to me trying to speed up startup time, the other was while removing use of final in asyncio (it’s still there in the typeshed, only removed in the implementation.)

For anyone curious, this isn’t a reliable attribute for runtime typecheckers as the standard lib implementation just tries to set it to True, and swallows exceptions related to setting it. If runtime enforcement is desired, you should raise in __init_subclass__ which is reliable.

With that said, I’d rather focus on a path toward the typing module not being fundamentally at odds with people that have a reason to care about import time.

1 Like

Maybe a bit tangential, but — huh, where do you get 45ms from? I see 10x less than that (Python 3.13.1, macOS laptop but similar absolute timing on a Linux server).

λ hyperfine '/Users/shantanu/.pyenv/versions/3.13.1/bin/python3.13 -c "import typing"' '/Users/shantanu/.pyenv/versions/3.13.1/bin/python3.13 -c "pass"' 
Benchmark 1: /Users/shantanu/.pyenv/versions/3.13.1/bin/python3.13 -c "import typing"
  Time (mean ± σ):      22.3 ms ±   3.4 ms    [User: 11.6 ms, System: 3.7 ms]
  Range (min … max):     9.5 ms …  28.1 ms    100 runs
 
Benchmark 2: /Users/shantanu/.pyenv/versions/3.13.1/bin/python3.13 -c "pass"
  Time (mean ± σ):      18.6 ms ±   4.1 ms    [User: 8.4 ms, System: 3.1 ms]
  Range (min … max):     5.4 ms …  27.1 ms    90 runs
 
Summary
  /Users/shantanu/.pyenv/versions/3.13.1/bin/python3.13 -c "pass" ran
    1.20 ± 0.32 times faster than /Users/shantanu/.pyenv/versions/3.13.1/bin/python3.13 -c "import typing"

I’m seeing anywhere from 17ms (which is closer to what you are seeing) to 65ms now on the 3 environments I have available right now. I suspect there’s some level of os caching involved in it varying so much,especially with repeated checks. The 17ms was on a machine that can build tensorflow from source in a reasonable amount of time.

The 45ms number came from setting PYTHONPROFILEIMPORTTIME=1 (env equivalent to -X importtime`) in CI at work for a few hours, and that was slightly under the lowest typing came in at all day. I can try and get an actual check of this in the staging environment some time next week, but I’m not running stuff where import time matters on machines with an m3

1 Like

typing isn’t the biggest issue to startup time I’ve seen, but it seems worse than other modules having import costs since the cost is being paid when any dependency adds inline typehints and is paid even by people not using typing at runtime.

If and when you get updated info, I’d be interested in any sharable details about the system specifications, as well as how much of the time is executing code in the typing module and how much of that time is other things the typing module imports. Maybe this is possible to improve in multiple ways, including making import typing faster

Importing typing on this macOS M2 (python3.14 -X importtime -c 'import typing') is also slower with Python 3.14 (taking 5 ms) due to it importing the new annotationlib (itself taking 4 ms, mostly due to importing ast), compared to 3ms for typing on 3.13 (python3.13 -X importtime -c 'import typing').

And with hyperfine:

python3.13 -c "import typing" ran
    1.16 ± 0.46 times faster than python3.14 -c "import typing"

Compared to:

  python3.14 -c "pass" ran
    1.15 ± 0.07 times faster than python3.13 -c "pass"

I’m going to refocus this, along with a few other points about typing module and runtime cost, into a new thread that can address each with more concrete solutions proposed rather than punting on possible solutions, I’m going to ask that a moderator close this one.

Closed at request of the OP.

1 Like