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 qualifierFinal
to the type. The spelling asnoimport 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]