-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] Add support for Literals in implicit type aliases
#21296
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
base: main
Are you sure you want to change the base?
Conversation
Diagnostic diff on typing conformance testsChanges were detected when running ty on typing conformance tests--- old-output.txt 2025-11-07 16:35:19.406661502 +0000
+++ new-output.txt 2025-11-07 16:35:22.836690933 +0000
@@ -16,7 +16,6 @@
aliases_explicit.py:59:5: error[type-assertion-failure] Argument does not have asserted type `int | str | None | list[list[int]]`
aliases_explicit.py:60:5: error[type-assertion-failure] Argument does not have asserted type `(...) -> None`
aliases_explicit.py:61:5: error[type-assertion-failure] Argument does not have asserted type `int | str`
-aliases_explicit.py:63:5: error[type-assertion-failure] Argument does not have asserted type `Literal[3, 4, 5] | None`
aliases_explicit.py:98:1: error[type-assertion-failure] Argument does not have asserted type `list[str]`
aliases_explicit.py:101:6: error[call-non-callable] Object of type `UnionType` is not callable
aliases_implicit.py:54:24: error[invalid-type-form] List literals are not allowed in this context in a type expression: Did you mean `tuple[str, str]`?
@@ -1002,5 +1001,5 @@
typeddicts_usage.py:28:17: error[missing-typed-dict-key] Missing required key 'name' in TypedDict `Movie` constructor
typeddicts_usage.py:28:18: error[invalid-key] Invalid key for TypedDict `Movie`: Unknown key "title"
typeddicts_usage.py:40:24: error[invalid-type-form] The special form `typing.TypedDict` is not allowed in type expressions. Did you mean to use a concrete TypedDict or `collections.abc.Mapping[str, object]` instead?
-Found 1004 diagnostics
+Found 1003 diagnostics
WARN A fatal error occurred while checking some files. Not all project files were analyzed. See the diagnostics list above for details. |
|
|
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
invalid-argument-type |
98 | 2 | 13 |
unused-ignore-comment |
2 | 72 | 0 |
no-matching-overload |
28 | 1 | 0 |
unresolved-attribute |
8 | 17 | 0 |
invalid-assignment |
14 | 1 | 4 |
unsupported-operator |
9 | 2 | 2 |
invalid-return-type |
3 | 4 | 4 |
type-assertion-failure |
0 | 11 | 0 |
redundant-cast |
5 | 3 | 0 |
invalid-type-form |
6 | 0 | 0 |
possibly-missing-attribute |
0 | 3 | 3 |
possibly-unresolved-reference |
0 | 3 | 0 |
division-by-zero |
2 | 0 | 0 |
invalid-context-manager |
0 | 0 | 2 |
invalid-parameter-default |
1 | 0 | 0 |
not-iterable |
0 | 0 | 1 |
unknown-argument |
0 | 1 | 0 |
| Total | 176 | 120 | 29 |
CodSpeed Performance ReportMerging #21296 will degrade performances by 5.31%Comparing Summary
Benchmarks breakdown
Footnotes
|
85ddb08 to
457167e
Compare
457167e to
cdd13e6
Compare
| return Type::KnownInstance(KnownInstanceType::Literal(TypeList::singleton( | ||
| self.db(), | ||
| result, | ||
| ))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the TypeList type is useful, but Literal should probably use a simpler wrapper that only contains a single type. Optional and Annotated also just need a single element. I will change this in a follow-up PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because in a case like Literal[1, 2, 3], it's still just a "single" union type?
carljm
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, modulo a couple things about diagnostics. Thank you!
crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md
Outdated
Show resolved
Hide resolved
| # error: [invalid-type-form] "`typing.Literal` instances are not allowed in type expressions" | ||
| def _(weird: IntLiteral1[int]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have two thoughts about this error message.
- It's a little weird to talk about "
typing.Literalinstances", because at runtime and in typeshed,typing.Literalis not a class and there are no instances of it. It's a special form, and indexing it returns an instance oftyping._LiteralGenericAlias. I think maybe a term that would hew a bit closer to runtime, and align more with the user's likely intuition thatT = Literal[1]defines a type alias, would be to call them "typing.Literalaliases", maybe?
- The error message doesn't quite match what I see happening in the code? What you are describing as a "
typing.Literalinstance" (and I suggest calling a "typing.Literalalias") isIntLiteral1-- and that most certainly is allowed in a type expression! What isn't allowed is to subscript it again. So the error I'd expect to see here would be more like "typing.Literalaliases cannot be subscripted." (For reference, pyright gives the somewhat obscure "Expected no type arguments for class int", where I think "class int" results from an implicit promotion from the literal type, and mypy gives "Bad number of arguments for type alias, expected 0, got 1"). Both of these indicate that the mistake is notIntLiteral1appearing in a type expression, it's the attempt to subscript it (or pass it type arguments).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, thank you. I changed it and the error message now reads:
[invalid-type-form] "
Literal[26]is not a generic class"
crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
Outdated
Show resolved
Hide resolved
| return Type::KnownInstance(KnownInstanceType::Literal(TypeList::singleton( | ||
| self.db(), | ||
| result, | ||
| ))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because in a case like Literal[1, 2, 3], it's still just a "single" union type?
AlexWaygood
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! It could possibly also be worth adding tests that demonstrate that this also now works:
from typing import Union, Literal
X = Literal[2, 3]
def f(x: Union[X, str]):
reveal_type(x) # revealed: Literal[2, 3] | str| } | ||
|
|
||
| /// An instance of `types.UnionType`. | ||
| /// A salsa-interned list of types. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not wild about the fact that -- depending on context -- this type can sometimes hold a list of value-expression types (if it's the wrapped data inside a KnownInstance::UnionType variant) and sometimes hold a list of type-expression types (if it's the wrapped data inside a KnownInstance::Literal variant). That feels a little bug-prone? E.g. it would be incorrect to call the .to_union() method on a TypeList wrapped inside a KnownInstance::Union variant, because you need to first convert them all to type expressions. But it would be incorrect to convert them all to type expressions on the elements in a TypeList inside a KnownInstance::Literal variant, because those have all already been interpreted as type expressions eagerly when building the type list.
I think I'd prefer to have different structs for the wrapped data inside the two variants, since they have pretty different semantics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. I will change this in major ways in my next PR, and will keep this in mind.
| self.infer_type_expression(slice); | ||
| if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { | ||
| builder.into_diagnostic(format_args!( | ||
| "`typing.Literal` instances are not allowed in type expressions", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly, we only reach this branch for cases like these:
from typing import Literal
X = Literal[1, 2]
def f(a: X[int], b: Literal["foo"][str]):
...In which case, I think I'd prefer something a bit closer to the runtime error message, which feels clearer to me:
>>> from typing import Literal
>>> Literal[2][int]
Traceback (most recent call last):
File "<python-input-1>", line 1, in <module>
Literal[2][int]
~~~~~~~~~~^^^^^
File "/Users/alexw/.pyenv/versions/3.13.1/lib/python3.13/typing.py", line 432, in inner
return func(*args, **kwds)
File "/Users/alexw/.pyenv/versions/3.13.1/lib/python3.13/typing.py", line 1473, in __getitem__
raise TypeError(f"{self} is not a generic class")
TypeError: typing.Literal[2] is not a generic classThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above, the error message now reads:
[invalid-type-form] "
Literal[26]is not a generic class"
Summary
Add support for
Literaltypes in implicit type aliases.part of astral-sh/ty#221
Ecosystem analysis
This looks good to me, true positives and known problems.
Test Plan
New Markdown tests.