Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Treat alternative syntax for unions (and other "new-semantics-old-syntax" typing features) in explicit TypeAliases inside if TYPE_CHECKING blocks as stringized annotations. #1562

Open
elenakrittik opened this issue Jan 7, 2024 · 6 comments
Labels
topic: feature Discussions about new features for Python's type annotations

Comments

@elenakrittik
Copy link

elenakrittik commented Jan 7, 2024

The Problem

At runtime, TYPE_CHECKING is always False, only type checkers assume it is True. Therefore, if TYPE_CHECKING blocks are only parsed, but never evaluated by the Python interpreter. Since A = int | str is a valid syntax for all still-supported Python versions at the moment (all versions ever since typing was introduced, actually), it would make sense if type checkers were allowed to accept usage of "new-semantics-old-syntax" features (like the alternative union syntax) in explicitly-annotated TypeAliases inside of if TYPE_CHECKING blocks as long as the Python version specified [project.requires-python] (or it's equivalent in tools) can successfully parse that syntax. This is essentially the same as treating explicitly-annotated TypeAliases as stringized annotations by-default.

Examples

# Run on 3.9

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    A = int | str  # Alternative syntax for unions requires Python 3.10 or newer # pyright
                   # Invalid type alias: expression is not a valid type          # mypy
                   # Unsupported left operand type for | ("type[int]")           # mypy

# With proposal
from typing import TYPE_CHECKING
from typing_extensions import TypeAlias

if TYPE_CHECKING:
    A: TypeAlias = int | str  # Treated as a stringized annotation because `: TypeAlias`
                              # is explicit, therefore valid.

pyright-play
mypy-play

@elenakrittik elenakrittik added the topic: feature Discussions about new features for Python's type annotations label Jan 7, 2024
@Daverball
Copy link
Contributor

Daverball commented Jan 7, 2024

I'd support this given that type checkers already happily accept this union syntax in type annotations if from __future__ import annotations is used. Same goes for subscriptability of builtin types prior to 3.9. And this is arguably the safer case to stay quiet about compared to the other one, which will raise an exception when inspected at runtime with e.g. typing_extensions.get_type_hints.

I don't think the stipulation with project.requires-python is necessary. mypy already supports type checking for all versions of python, regardless of which python version it was originally installed in. (although I'm not sure if that also applies to the new PEP695 syntax, but that is irrelevant for this change, since it's not old syntax, so I don't think it matters either way)

@erictraut
Copy link
Collaborator

I'm strongly opposed to this proposal. The whole intention behind TYPE_CHECKING is that it provides a way to "lie" to the type checker about what is seen at runtime. What you're proposing here is to give it some special meaning that would require a bunch of extra special-casing in a type checker.

I recommend against using TYPE_CHECKING unless there's no way around it. Lying to the type checker can mask errors that would otherwise be caught through static analysis, so it often makes code more fragile.

If you want to define a type alias using the pre-3.10 syntax, you can do so safely without the use of TYPE_CHECKING by doing the following:

A: TypeAlias = "int | str"

@elenakrittik
Copy link
Author

elenakrittik commented Jan 8, 2024

If you want to define a type alias using the pre-3.10 syntax, you can do so safely without the use of TYPE_CHECKING by doing the following:

A: TypeAlias = "int | str"

This is a good solution, thank you. My only (relatively small) complaint about it is that it is not commonly auto-completed in editors. I'd like to still proceed with the proposal if possible.

I'm strongly opposed to this proposal. The whole intention behind TYPE_CHECKING is that it provides a way to "lie" to the type checker about what is seen at runtime. What you're proposing here is to give it some special meaning that would require a bunch of extra special-casing in a type checker.

While it is true that this would require special-casing and i am not totally "happy" about it too, i don't see this as "lying" to the type-checker. This is what, in both practice and theory, should and does happen at runtime: syntax is valid, but the "semantics" are essentially ignored because TYPE_CHECKING is False at runtime.

I recommend against using TYPE_CHECKING unless there's no way around it. Lying to the type checker can mask errors that would otherwise be caught through static analysis, so it often makes code more fragile.

In most (all, actually, but i don't want to be rude) projects that i have seen, "lying" to the type-checker is not what TYPE_CHECKING is used for. PEP 484 also mentions example usages for it, and these are exactly what i'm used to seeing.

@elenakrittik
Copy link
Author

elenakrittik commented Jan 8, 2024

I don't think the stipulation with project.requires-python is necessary. mypy already supports type checking for all versions of python, regardless of which python version it was originally installed in. (although I'm not sure if that also applies to the new PEP695 syntax, but that is irrelevant for this change, since it's not old syntax, so I don't think it matters either way)

While mypy, pyright and others may support many Python versions at the same time, the same cannot be always said about the project being type-checked. This is not actually the case, but imagine if a | b was an invalid syntax in Python 3.9. If a given project claims to support 3.9+, then the type-checker must report any usage of it in the project, regardless of it being in if TYPE_CHECKING or not, as the syntax itself is invalid and Python 3.9 will not be able to parse it, and therefore the claim of 3.9+ support will render invalid.

@Daverball
Copy link
Contributor

Daverball commented Jan 8, 2024

While mypy, pyright and others may support many Python versions at the same time, the same cannot be always said about the project being type-checked. This is not actually the case, but imagine if a | b was an invalid syntax in Python 3.9. If a given project claims to support 3.9+, then the type-checker must report any usage of it in the project, regardless of it being in if TYPE_CHECKING or not, as the syntax itself is invalid and Python 3.9 will not be able to parse it, and therefore the claim of 3.9+ support will render invalid.

You could've used PEP695 as an example, in that case as soon as you use it your project can only support Python 3.12+. I understand what you mean, but it's not really related to your proposal, since it specifically mentions valid syntax with new semantics, i.e. stuff that would throw exceptions at runtime but parses just fine, so I don't see how respecting the Python version in project.requires-python enters into the proposal, you tell the type checker which version(s) you want to be compatible with and can check each of them individually.

If a type expression is evaluated at runtime it's also part of what the type checker evaluates, so if it sees operations that aren't valid for that version of Python it will (or at least should) complain. But there is the existing exception for forward references (explicit via wrapping in string, or implicit via from __future__ import annotations), which is basically the same exception you're asking for.

I can see why unmarked type aliases can be a bit problematic for this proposal, since you essentially need to defer raising an error about invalid operations until you're absolutely certain that the expression is not a type alias. But in an explicit type alias using TypeAlias I don't think it would be that much to ask for, it's on the same level of complexity as the existing exception.

@elenakrittik elenakrittik changed the title Allow usage of alternative syntax for unions (and other "new-semantics-old-syntax" typing features) inside if TYPE_CHECKING blocks if the minimum Python version a given project claims to support can successfully parse feature's syntax. Treat alternative syntax for unions (and other "new-semantics-old-syntax" typing features) in explicit TypeAliases inside if TYPE_CHECKING blocks as stringized annotations. Jan 9, 2024
@elenakrittik
Copy link
Author

@Daverball Appreciate the explanation, this indeed does make sense! I edited the original message to mention these, let me know if i got something wrong!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: feature Discussions about new features for Python's type annotations
Projects
None yet
Development

No branches or pull requests

3 participants