Hi everyone!
We have a problem with type hints when we need to import objects from other modules. It can lead to cyclic imports, longer import times, and increased complexity.
Currently it can be solved with imports guarded by if TYPE_CHECKING
:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import module_a
from module_b import b_class
But it does not fully solve the problem. Because it does not actually evaluate to any runtime object. So, this will fail:
>>> from typing import TYPE_CHECKING, get_type_hints
>>> if TYPE_CHECKING:
... from email.message import Message
...
>>> def send(msg: Message) -> None: ...
...
>>> get_type_hints(send)
Traceback (most recent call last):
File "<python-input-4>", line 1, in <module>
get_type_hints(send)
~~~~~~~~~~~~~~^^^^^^
File "<python-input-3>", line 1, in __annotate__
def send(msg: Message) -> None: ...
^^^^^^^
NameError: name 'Message' is not defined
In some cases it can be a 'Message'
string when explicit strings or from future import annotations
are used. This is not ideal because it will also fail with NameError
when calling get_type_hints
.
Currently PEP-649 and PEP-749 provide a new way of dealing with such objects: ForwardRef
.
I propose to add a new syntax and a new runtime feature: type import
:
from email.message import type Message
import type os
At runtime these new objects will be ForwardRef
objects with proper context provided by the compiler that will natively work with any existing annotationlib.get_annotations
or typing.get_type_hints
calls.
During type checking, these new objects will be considered a special object, which can be used in annotations only.
Grammar changes
The grammar for type imports should be the same as regular import
and from import
.
Including the *
part and all other rules.
We can differentiate whether the optional type
soft keyword was provided or not. If provided, we can set the new is_type_only
attribute to True
, which would be False
by default.
Compiler changes
Currently, ForwardRef
is a Python object. But, we can teach the compiler to call a different (instinct?) bytecode instruction instead of IMPORT_NAME
and IMPORT_FROM
to get a ForwardRef
. Or create new IMPORT_NAME_TYPE
and IMPORT_FROM_TYPE
instructions if it would be better for optimizations.
Typing spec changes
This section is only for type checkers and type-checking semantics of type-only imports.
Using names from type-only imports should be allowed in all annotations: functions, classes, inline, type aliases, etc.
But using type-only names in other contexts should not be allowed:
from mod type import A
x: A # ok, used as annotation
A(1, 2) # should raise a type checking error
Because at runtime it will also fail:
Traceback (most recent call last):
File "<python-input-9>", line 1, in <module>
A(1, 2)
~^^^^^^
TypeError: 'ForwardRef' object is not callable