Introduce type keyword for type only imports

After the great work of everyone involved in PEP 810, I would like to propose a new idea that is similar and somewhat changes a bit of the PEP itself, which is the introduction of the type keyword for imports that are only used for typing reasons.

This would be beneficial for people reading the code to understand that the import is only there for type checking reasons, and it will not ever be evaluated, similar to lazy imports proposed in PEP 810, but the code won’t ever need to do the reification of the import, because the code will never call the imported item.

For example:

from typing import type Dict

I understand there is a type keyword already implemented, so I’m unsure if there is a compatibility issue here, so maybe another keyword should be used instead. I thought of the following:

  • typeonly
  • typedef
  • ty (although clashes with the ty project)

This implementation could have the following benefits:

  • Type imports would never be evaluated
  • Optionally, libs could be bundled without type imports, lowering the size of the distribution (?)
  • Improve readability, as it would be clear the imports are there only for type checking
  • Possibly improve linters and static type checkers, depending on the implementation
  • Removes TYPE_CHECKING requirement for cases where imports could cause a recursive dependency
  • Multiple codebases import typings without the TYPE_CHECKING check - this would be beneficial for the 1st point

My idea is that this would move the type imports after all the other imports in the file, as such:

import json

from datetime import datetime

from typing import type Dict

For backwards compatibility, possibly the CPython implementation could replace it with the current TYPE_CHECKING check.

I should make it clear that this is only useful after (if) PEP 810 is implemented, as this could create a confusion between real lazy imports, and imports that are tagged as lazy but are just there for type checking reasons.

Would be great to hear ideas, suggestions, questions or general feedback.

Thanks!

See also: PEP 810 discussion

3 Likes

This is an assumption I am really opposed to baking into the language. Runtime type checkers definitely want to evaluate them.

2 Likes

As far as I understand, this would not change how it currently happens with if TYPE_CHECKING, since in runtime, it’s always false. Please let me know if there’s another case I’m not seeing here.

Thanks

Yes, and I don’t like if TYPE_CHECKING for that reason. IMO lazy imports provide a good alternative for many situations, I don’t understand why we need to go further in making runtime type checkers difficult to implement.

(technically they can also set TYPE_CHECKING to true at runtime, but I don’t think they actually do this)

The only real difference you have listed to PEP 810 is that these never get evaluated, so I am assuming you are making this evaluation impossible. Why? Why not just let them be evaluated and use the syntax and semantics of PEP 810? What do you really gain from removing this option? It’s not performance.

1 Like

I don’t see much advantage over the general lazy import syntax.

Why not? What happens if you evaluate some annotations that rely on a typing-only import.

6 Likes

Just to be clear, the main intention of this suggestion is not the performance, obviously.

It is more focused on code readability. Since the lazy keyword implementation enables this to be used with types as well, reading a code that imports a typing class with the lazy keyword might indicate for the reader that it will be evaluated at some point in the future, which is not necessarily true.

Clarifying even more, this would only apply if PEP 810 was implemented, as a way to differentiate real lazy imports from type imports.

The idea is to make it clear for the reader that the import is only for type checking, since the lazy keyword can be used as well, and I believe this could create a confusion (more details in my reply above).

Regarding the annotations, I’m not sure what you mean with that, could you please give an example?

Thanks for the feedbacks!

The confusion is larger if both options are available.

If PEP 810 is introduced, the recommendation becomes: use lazy imports. That’s it. No need to mention TYPE_CHECKING for 99% of cases.

If we also get type imports the recommendation becomes:

  • If an object is truly only used in non-evaluated contexts (i.e. string-based forward references, type statements, annotations), use type import
  • If you use old-style TypeAlias at top level because the new type alias statement is not acceptable, use eager imports. (since it’s at top level most likely and would be imported at that point anyway)
  • if you use the type outside of a typing context, you may want to use lazy imports

Oh, and if you introduce new non-typing uses of a class, e.g. isinstance checks, don’t forget to update the import statement! It was imported the wrong way for runtime use.


Adding type imports that do not actually resolve to the runtime object is just asking for an enormous amount of confusion. And introduce identical semantics with two different keywords just isn’t worth anything. Yes, it’s maybe slightly more intuitive. But people are always going to ask “well, what’s the difference” and answering “nothing” isn’t exactly the most satisfying answer. Especially because of the Zen of Python.

2 Likes

Generally I don’t think this is necessary, but one technical question:

What does

from typing import type Dict, Tuple

mean?

Matěj

1 Like

Small remark: I don’t want to comment on whether this is necessary, but if we do this, I suggest to use a single new keyword importtype or import_type rather than the compound import type.

This makes it more obvious that this is not a regular import. It also prevents collisions with the existing bulitin type (note that from builtins import type is valid Python code right now).

2 Likes

I generally think a lazy import mechanism is mostly sufficient for all existing use cases, and like others, do not think that forbidding evaluation of annotations is a good thing.

There are already many patterns that break runtime typechecking for the sake of performance because they use if TYPE_CHECKING: ... without a safe fallback value in the else branch, we should be striving for fewer cases like this, not more.

I would be mildly in favor of allowing combining type alias statements with imports as

type import foo

with no support for from / as imports, requiring secondary aliasing such as:

type import typing
type Any = typing.Any

as a signal that these are only intended for use in typing contexts, (and which would have some benefit, as wrapping like this prevents accidental use as a value in non-typing aware contexts, without locking out runtime typecheckers) but between this and pep 810, if we only have one, I would strongly prefer 810. The more general solution is more valuable.

4 Likes