Current blockers on runtime deferal of typing imports

As part of my recent efforts to reduce import times across modules, and reduce the need to directly import typing, a few things in typing stand out, especially where it interacts with static type checkers currently.

Some of the below status quo solutions also rely on TYPE_CHECKING, which is currently only safe by convention, not by specification, see related discussion here: Specify `TYPE_CHECKING = False` without typing import

As those changes are much less transformative and apply to something that already mostly works by convention, I won’t rehash the discussion here.

typing.final

Relevant current info

The decorator at runtime attempts to set __final__ = True and then return the modified object.

Because it is a decorator, it has a runtime effect that is part of the type declaration and cannot be deferred in any way. The current best option is:

TYPE_CHECKING = False
if TYPE_CHECKING:
    from typing import final
else:
    final = lambda f: f  
    # This isn't runtime reliable anyway, don't bother setting __final__

Proposed change

If __final__ = True is present in a class declaration, the class should be treated as Final by type checkers. There is no intent to deprecate or soft deprecate typing.final as a decorator, existing code should only have churn from this that matches user pressure.

Example:

class Example:
    __final___ = True

class ShouldError(Example):  # error here
    ...

Runtime enforcement of this can be achieved with __init_subclass__ use, however detecting this via the return type type of __init_subclass__ being Never would reintroduce a dependency on typing (for Never). As an annotation, this can be safely deferred, but requires a lot of legwork[1].

typing.Final

Relevant current info

As brought up previously[1:1], less constructively, type checkers currently require this to be imported directly from typing. Most tricks don’t work here, even those that do defer correctly for runtime.

Neat tricks like the below don’t work at static analysis time (they do work at runtime). Of the symbols here, Final is the only one that does not work.

# _typings.py
from __future__ import annotations

TYPE_CHECKING = False

if TYPE_CHECKING:
    from typing import Any, ClassVar, Final, Literal, Never, Self, final, overload
else:
    overload = final = lambda f: f

def __getattr__(name: str):

    if name in {"Any", "ClassVar", "Final", "Literal", "Never", "Self"}:
        import typing

        return getattr(typing, name)

    msg = f"module {__name__!r} has no attribute {name!r}"
    raise AttributeError(msg)


__all__ = ["Any", "ClassVar", "Final", "Literal", "Never", "Self", "final", "overload"]
# some other module
from __future__ import annotations

# importing a re-exported name directly would make typing import eager
from collections.abc import Callable, Coroutine
from . import _typings as t  

type CoroFunc[**P, R = t.Any] = Callable[P, Coroutine[None, None, R]]

class SomethingEncodable:
    type_id: t.Final[int] = 42
    ...

Some type checkers don’t understand this, by design see: False positives when deffering import of typing special forms · Issue #9664 · microsoft/pyright · GitHub

Others do, but require configuration: False positives when defering import of `typing.Literal` · Issue #15306 · astral-sh/ruff · GitHub

Any method that makes the name exist in the module would be eagerly evaluated. This leads to a modification of the above working if you want to preserve laziness, but this is much more intrusive as it requires a repeated conditional in every file due to the non-understanding

# some module
TYPE_CHECKING = False
if TYPE_CHECKING:
    import typing as t
else:
    from . import _typings as t  # same module definition as above

class SomethingEncodable:
    type_id: t.Final[int] = 42
    ...

Proposed changes

  • type checkers must by specification understand re-exports from typing. It is not enough to provide configuration for this, as this would not be possible to use in library typings (libraries that include py.typed)

  • If the the above is something that pyright would try to block, then # type: noimport Final without Final being an imported should be understood to mean to add the type qualifier Final to the type. The spelling as noimport Final is necessary due to potential ambiguity with the old syntax for types pre-annotations.

typing.overload

Relevant current info

This has runtime effects in registering overloads. The resolution of overloads at runtime is already not guaranteed, as it doesn’t work for stubs. This one’s hard to do much about, and I’m fine with the existing solution.

TYPE_CHECKING = False
if TYPE_CHECKING:
    from typing import overload
else:
    overload = lamba f: f

The above __getattr__ based solution includes this.

Proposed Change

Add a section to the typing guide about this, and make sure it’s clearly documented that this is a tradeoff in runtime resolution of overloads via typing.get_overloads

typing.NamedTuple

Current status quo

This has runtime evaluation, and unlike the others, the runtime evaluation is always going to be important.

Proposed changes

None seem needed here, but it may be worth documenting what NamedTuple’s desugar to for anyone to write their own by hand in places where it matters.

typing.cast (edited in, see below)

Current relevant info

Similar to typing.final, this is mostly just for the benefit of static analysis with no intended runtime impact.

The current option:

TYPE_CHECKING = False
if TYPE_CHECKING:
    from typing import cast
else:
    cast = lambda typ, val: val

proposed change

This one has been previously discussed as being the only reason people have any typing imports, and wanting ways to not do so, including removing the function call altogether

Syntax to remove the cast entirely at runtime would be welcome, but the syntax proposed in that thread won’t work as it conflicts with context managers.

a type comment to act as a cast would partially work, but wouldn’t help if it happens deep in a nested expression.

Other possible improvements (edited in)

overload and final are essentially identity functions when a user bypasses their secondary effects. exposing the c implementation of an identity function as commented below[2] would help reduce the runtime cost of these calls, but these are generally only incurred once, at declaration, so a comparison of import time versus lambda declaration time would be valuable.

cast is not quite an identity function, but exposing a c implementation somewhere cheaper to import from than typing, such as [in the operator module as commented below[2:1], would be more likely to be an improvement as casts are generally done in functions which may get called repeatedly.

This however is distinctly separate and needs further discussion elsewhere as these are changes not to the typing specification and what users can expect to be able to do, but runtime additions. It also has been rejected repeatedly before[3]


  1. Static analysis at odds with runtime goals ↩︎ ↩︎

  2. Current blockers on runtime deferal of typing imports - #4 by AA-Turner ↩︎ ↩︎

  3. add identity function · Issue #44652 · python/cpython · GitHub ↩︎

7 Likes

I’m a bit confused by this. What does @overload do at runtime?

I thought it was a purely static typing thing.

It attempts to store the function’s name and line number for use by get_overloads()

See also clear_overloads() (both new in 3.11)

A

@mikeshardmind I would also mention typing.cast(). No runtime effect, just an identity function.

TYPE_CHECKING = False
if TYPE_CHECKING:
    from typing import cast
else:
    cast = lambda typ, v: v

As an aside, several of these seem to (at runtime) reduce to an identity function of some sort, returning the input unchanged. We added a C version of this as _typing._idfunc for use in typing.NewType.__call__, perhaps this could be made public (operator.identity_function?) for these sort of cases?

Would likely need to be a separate proposal since this would be a runtime change rather than just typing specification, but operator would be a great place to put it. It’s relatively cheap as a module, and it’s also pretty deep in the import hierarchy of a ton of the standard library already.

I’ll edit the above with a small detail about it as an option for such identity function, as well as add that cast has a similar situation as final as a decorator.

There is an old issue for this. I think it has been suggested (and rejected again) multiple times since. I would definitely support adding an identity function but best to keep that as separate from this thread I think.