Conversation
Diagnostic diff on typing conformance testsChanges were detected when running ty on typing conformance tests--- old-output.txt 2025-08-05 08:54:39.534836544 +0000
+++ new-output.txt 2025-08-05 08:54:39.597836730 +0000
@@ -137,6 +137,7 @@
callables_annotation.py:57:18: error[invalid-type-form] List literals are not allowed in this context in a type expression: Did you mean `list[int]`?
callables_annotation.py:58:5: error[invalid-type-form] Special form `typing.Callable` expected exactly two arguments (parameter types and return type)
callables_annotation.py:58:14: error[invalid-type-form] The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`
+callables_annotation.py:114:14: error[invalid-argument-type] `ParamSpec` is not a valid argument to `Protocol`
callables_kwargs.py:24:5: error[type-assertion-failure] Argument does not have asserted type `int`
callables_kwargs.py:32:9: error[type-assertion-failure] Argument does not have asserted type `str`
callables_kwargs.py:35:5: error[type-assertion-failure] Argument does not have asserted type `str`
@@ -147,9 +148,12 @@
callables_kwargs.py:65:5: error[missing-argument] No argument provided for required parameter `v3` of function `func2`
callables_protocol.py:97:1: error[invalid-assignment] Object of type `def cb4_bad1(x: int) -> None` is not assignable to `Proto4`
callables_protocol.py:121:1: error[invalid-assignment] Object of type `def cb6_bad1(*vals: bytes, *, max_len: int | None = None) -> list[bytes]` is not assignable to `NotProto6`
+callables_protocol.py:176:14: error[invalid-argument-type] `ParamSpec` is not a valid argument to `Protocol`
callables_protocol.py:179:62: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `R@__call__`
callables_subtyping.py:26:5: error[invalid-assignment] Object of type `(int, /) -> int` is not assignable to `(int | float, /) -> int | float`
callables_subtyping.py:29:5: error[invalid-assignment] Object of type `(int | float, /) -> int | float` is not assignable to `(int, /) -> int`
+callables_subtyping.py:204:21: error[invalid-argument-type] `ParamSpec` is not a valid argument to `Protocol`
+classes_classvar.py:35:14: error[invalid-argument-type] `ParamSpec` is not a valid argument to `Generic`
classes_classvar.py:38:11: error[invalid-type-form] Type qualifier `typing.ClassVar` expected exactly 1 argument, got 2
classes_classvar.py:39:14: error[invalid-type-form] Int literals are not allowed in this context in a type expression
classes_classvar.py:40:14: error[unresolved-reference] Name `var` used when not defined
@@ -382,6 +386,7 @@
generics_defaults.py:55:1: error[type-assertion-failure] Argument does not have asserted type `@Todo`
generics_defaults.py:59:1: error[type-assertion-failure] Argument does not have asserted type `@Todo`
generics_defaults.py:63:1: error[type-assertion-failure] Argument does not have asserted type `@Todo`
+generics_defaults.py:76:23: error[invalid-argument-type] `ParamSpec` is not a valid argument to `Generic`
generics_defaults.py:79:1: error[type-assertion-failure] Argument does not have asserted type `@Todo`
generics_defaults.py:80:1: error[type-assertion-failure] Argument does not have asserted type `@Todo`
generics_defaults.py:91:26: error[invalid-argument-type] `@Todo` is not a valid argument to `Generic`
@@ -412,6 +417,7 @@
generics_paramspec_semantics.py:38:28: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int`
generics_paramspec_semantics.py:53:34: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int`
generics_paramspec_semantics.py:57:34: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int`
+generics_paramspec_semantics.py:67:9: error[invalid-argument-type] `ParamSpec` is not a valid argument to `Generic`
generics_paramspec_semantics.py:76:30: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `str`
generics_paramspec_semantics.py:82:5: error[type-assertion-failure] Argument does not have asserted type `@Todo`
generics_paramspec_semantics.py:82:28: error[invalid-type-form] List literals are not allowed in this context in a type expression: Did you mean `list[int]`?
@@ -424,9 +430,12 @@
generics_paramspec_semantics.py:133:23: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int`
generics_paramspec_semantics.py:138:29: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int`
generics_paramspec_semantics.py:143:25: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int`
+generics_paramspec_specialization.py:13:14: error[invalid-argument-type] `ParamSpec` is not a valid argument to `Generic`
+generics_paramspec_specialization.py:18:29: error[invalid-argument-type] `ParamSpec` is not a valid argument to `Generic`
generics_paramspec_specialization.py:32:27: error[invalid-type-form] List literals are not allowed in this context in a type expression: Did you mean `tuple[int, bool]`?
generics_paramspec_specialization.py:40:27: error[invalid-type-form] List literals are not allowed in this context in a type expression: Did you mean `tuple[()]`?
generics_paramspec_specialization.py:40:31: error[invalid-type-form] List literals are not allowed in this context in a type expression: Did you mean `tuple[()]`?
+generics_paramspec_specialization.py:48:14: error[invalid-argument-type] `ParamSpec` is not a valid argument to `Generic`
generics_paramspec_specialization.py:52:22: error[invalid-type-form] List literals are not allowed in this context in a type expression: Did you mean `tuple[int, str, bool]`?
generics_scoping.py:14:1: error[type-assertion-failure] Argument does not have asserted type `int`
generics_scoping.py:15:1: error[type-assertion-failure] Argument does not have asserted type `str`
@@ -532,6 +541,7 @@
generics_typevartuple_basic.py:16:26: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `tuple[@Todo, ...]`
generics_typevartuple_basic.py:23:13: error[invalid-argument-type] `@Todo` is not a valid argument to `Generic`
generics_typevartuple_basic.py:42:34: error[invalid-argument-type] Argument to bound method `__init__` is incorrect: Expected `tuple[@Todo, ...]`, found `Literal[1]`
+generics_typevartuple_basic.py:52:14: error[invalid-argument-type] `TypeVarTuple` is not a valid argument to `Generic`
generics_typevartuple_basic.py:65:27: error[unknown-argument] Argument `covariant` does not match any known parameter of function `__new__`
generics_typevartuple_basic.py:66:27: error[too-many-positional-arguments] Too many positional arguments to function `__new__`: expected 2, got 4
generics_typevartuple_basic.py:67:27: error[unknown-argument] Argument `bound` does not match any known parameter of function `__new__`
@@ -784,6 +794,7 @@
protocols_subtyping.py:16:6: error[call-non-callable] Cannot instantiate class `Proto1`: This call will raise `TypeError` at runtime
protocols_subtyping.py:38:5: error[invalid-assignment] Object of type `Proto2` is not assignable to `Concrete2`
protocols_subtyping.py:55:5: error[invalid-assignment] Object of type `Proto2` is not assignable to `Proto3`
+protocols_variance.py:84:16: error[invalid-argument-type] `ParamSpec` is not a valid argument to `Protocol`
protocols_variance.py:85:62: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `R@__call__`
qualifiers_annotated.py:43:17: error[invalid-type-form] List literals are not allowed in this context in a type expression: Did you mean `tuple[int, str]`?
qualifiers_annotated.py:44:17: error[invalid-type-form] Tuple literals are not allowed in this context in a type expression
@@ -885,4 +896,4 @@
tuples_type_form.py:36:1: error[invalid-assignment] Object of type `tuple[Literal[1], Literal[2], Literal[3], Literal[""]]` is not assignable to `tuple[int, ...]`
typeddicts_operations.py:60:1: error[type-assertion-failure] Argument does not have asserted type `str | None`
typeddicts_type_consistency.py:101:1: error[invalid-assignment] Object of type `Unknown | None` is not assignable to `str`
-Found 886 diagnostics
+Found 897 diagnostics |
|
CodSpeed Instrumentation Performance ReportMerging #19733 will not alter performanceComparing Summary
|
This comment was marked as off-topic.
This comment was marked as off-topic.
|
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
invalid-argument-type |
7 | 5 | 0 |
unused-ignore-comment |
4 | 3 | 0 |
possibly-unbound-attribute |
3 | 0 | 0 |
possibly-unresolved-reference |
0 | 2 | 0 |
invalid-assignment |
0 | 1 | 0 |
| Total | 14 | 11 | 0 |
Type variant for TypedDict
9a7eddb to
a2077a2
Compare
AlexWaygood
left a comment
There was a problem hiding this comment.
Thank you! TypedDict types are odd in a number of ways; there's a few things to think through here --
| reveal_type(Person.__total__) # revealed: bool | ||
| reveal_type(Person.__required_keys__) # revealed: frozenset[str] | ||
| reveal_type(Person.__optional_keys__) # revealed: frozenset[str] |
There was a problem hiding this comment.
is it worth also adding some tests that demonstrate that these attributes cannot be accessed from inhabitants of the TypedDict type (unlike most ClassVars)?
There was a problem hiding this comment.
Yes, thanks. I added a test, but it fails (added a TODO). No other type checker gets this right, so I'm not going to invest time right now.
| Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db), | ||
| Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db), | ||
| Type::ProtocolInstance(protocol) => protocol.to_meta_type(db), | ||
| Type::TypedDict(typed_dict) => SubclassOfType::from(db, typed_dict.defining_class(db)), |
There was a problem hiding this comment.
I'm not sure this is correct, because at runtime TypedDict inhabitants are always instances of exactly dict. We use Type::to_meta_type() to infer the type of things like type(x) and x.__class__ -- if x is an inhabitant of a TypedDict type, both of those will be <class 'dict'> at runtime. Which would imply that this should be
| Type::TypedDict(typed_dict) => SubclassOfType::from(db, typed_dict.defining_class(db)), | |
| Type::TypedDict(typed_dict) => KnownClass::Dict.to_class_literal(db), |
There was a problem hiding this comment.
I don't think so. This way, we would lose important information. Sure, inhabitants are instances of dict at runtime. But a Type::TypedDict also represents a (special sub)set of dict instances. And its meta type should be the specific class that describes that subset.
Consider what happens if we access person["name"] on person: Person. Under the hood, we call type(person).__getitem__(person, "name"). If we model type(person) as being of type <class 'dict'>, we would get a generic answer like object. But we want this to be the type of the name item on Person.
Other type checkers also infer type[Person] for type(Person) (except for mypy, which infers Person as a function).
There was a problem hiding this comment.
Hmm, okay, interesting. I think what this maybe implies in that case is that we'll need to treat type[] types quite differently for TypedDicts than for other classes: you can only access the synthesised TypedDict methods (and any methods from Mapping) on type(x) (where x is a TypedDict inhabitant) — you can't access __required_keys__ and the other ClassVars on _TypedDictFallback from type(x)
There was a problem hiding this comment.
That's a good observation, yes. I attempted to fix this and found a pre-existing bug related to annotated assignments without a right hand side (but with type qualifiers) in stub files. As this relates to the __total__: ClassVar[bool] annotations in TypedDictFallback, I need to fix this first. I'd like to do that in a separate PR as it might have a wider-spread impact.
| | Type::Dynamic(..) | ||
| | Type::Never => { | ||
| | Type::Never | ||
| | Type::TypedDict(_) => { |
There was a problem hiding this comment.
they're always instances of exactly dict at runtime, but it may not be worth the complexity trying to model that here
| Type::TypedDict(_) => { | ||
| if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) { | ||
| self.extend_with_class_members(db, ty, class_literal); | ||
| } | ||
|
|
||
| self.extend_with_type(db, KnownClass::TypedDictFallback.to_instance(db)); | ||
| } |
There was a problem hiding this comment.
(You'll need to change this too if you think my comments above about the meta-type of TypedDicts is reasonable)
| | Type::PropertyInstance(_) | ||
| | Type::TypeIs(_) => None, | ||
| | Type::TypeIs(_) | ||
| | Type::TypedDict(_) => None, |
There was a problem hiding this comment.
Hmm, shouldn't this fallback to attributes of dict? (But I must be missing something, since it seems like similar should be true for many of the other types above in this arm.)
There was a problem hiding this comment.
find_name_in_mro only works on class-like types. It is conceptually similar to the following function:
def find_name_in_mro(cls, name, default):
"Emulate _PyType_Lookup() in Objects/typeobject.c"
for base in cls.__mro__:
if name in vars(base):
return vars(base)[name]
return defaultAccessing members on instance-like types will always go through class_member, which calls to_meta_type first (ty.class_member(…) = ty.to_meta_type(…).find_name_in_mro(…)), similar to how an attribute access of obj.attr is always type(obj).attr under the hood (except for instance attributes, which are handled elsewhere).
crates/ty_python_semantic/resources/mdtest/type_properties/truthiness.md
Show resolved
Hide resolved
AlexWaygood
left a comment
There was a problem hiding this comment.
LGTM if we're happy with the salsa bump!
@MichaReiser's PR has been merged and I updated the commit to the latest from salsa main. |
|
|
||
| For unions, `ide_support::all_members` only returns members that are available on all elements of |
There was a problem hiding this comment.
It looks like you maybe deleted the ### Unions header above accidentally here
| For unions, `ide_support::all_members` only returns members that are available on all elements of | |
| ### Unions | |
| For unions, `ide_support::all_members` only returns members that are available on all elements of |
There was a problem hiding this comment.
Ah, yeah. I noticed that to independently and fixed it in #19758
## Summary This PR fixes a few inaccuracies in attribute access on `TypedDict`s. It also changes the return type of `type(person)` to `type[dict[str, object]]` if `person: Person` is an inhabitant of a `TypedDict` `Person`. We still use `type[Person]` as the *meta type* of Person, however (see reasoning [here](#19733 (comment))). ## Test Plan Updated Markdown tests.
Summary
This PR adds a new
Type::TypedDictvariant. Before this PR, we treatedTypedDict-based types as dynamic Todo-types, and I originally planned to make this change a no-op. And we do in fact still treat that new variant similar to a dynamic type when it comes to type properties such as assignability and subtyping. But then I somehow tricked myself into implementing some of the things correctly, so here we are. The two main behavioral changes are: (1) we now also detect genericTypedDicts, which removes a few false positives in the ecosystem, and (2) we now support attribute access (not key-based indexing!) on these types, i.e. we infer proper types for something likeMyTypedDict.__required_keys__. Nothing exciting yet, but gets the infrastructure into place.Note that with this PR, the type of (the type)
MyTypedDictitself is still represented as aType::ClassLiteralorType::GenericAlias(in caseMyTypedDictis generic). Only inhabitants ofMyTypedDict(instances ofdictat runtime) are represented byType::TypedDict. We may want to revisit this decision in the future, if this turns out to be too error-prone. Right now, we need to use.is_typed_dict(db)in all the right places to distinguish between actual (generic) classes andTypedDicts. But so far, it seemed unnecessary to add additionalTypevariants for these as well.Note: this PR crucially depends on salsa-rs/salsa#954 by @MichaReiser. I'm not sure if we want to merge this before it has landed in salsa.(this is now merged, updated the commit ref to the latest commit on salsa main)part of astral-sh/ty#154
Ecosystem impact
The new diagnostics on
cloud-initlook like true positives to me.Test Plan
Updated and new Markdown tests