From 877e9613a8379c196c73aa28f1947596b129fa22 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 14 Nov 2025 09:49:22 +0100 Subject: [PATCH 1/6] [ty] Add synthetic members to completions on dataclasses --- .../mdtest/ide_support/all_members.md | 163 +++++++++++++++++- .../src/types/ide_support.rs | 73 +++++--- .../ty_python_semantic/src/types/instance.rs | 4 + 3 files changed, 215 insertions(+), 25 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md index e8c19625cae6a1..196cb59e75d4fd 100644 --- a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md +++ b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md @@ -548,13 +548,20 @@ static_assert(not has_member(c, "dynamic_attr")) ### Dataclasses -So far, we do not include synthetic members of dataclasses. +#### Basic + +For dataclasses, we make sure to include all synthesized members: + +```toml +[environment] +python-version = "3.9" +``` ```py from ty_extensions import has_member, static_assert from dataclasses import dataclass -@dataclass(order=True) +@dataclass class Person: age: int name: str @@ -562,13 +569,161 @@ class Person: static_assert(has_member(Person, "name")) static_assert(has_member(Person, "age")) +static_assert(has_member(Person, "__dataclass_fields__")) + # These are always available, since they are also defined on `object`: static_assert(has_member(Person, "__init__")) static_assert(has_member(Person, "__repr__")) static_assert(has_member(Person, "__eq__")) +static_assert(has_member(Person, "__ne__")) + +# There are not available, unless `order=True` is set: +static_assert(not has_member(Person, "__lt__")) +static_assert(not has_member(Person, "__le__")) +static_assert(not has_member(Person, "__gt__")) +static_assert(not has_member(Person, "__ge__")) + +# These are not available, unless `slots=True`, `weakref_slot=True` are set: +static_assert(not has_member(Person, "__slots__")) +static_assert(not has_member(Person, "__weakref__")) + +# Not available before Python 3.13: +static_assert(not has_member(Person, "__replace__")) +``` + +The same behavior applies to instances of dataclasses: + +```py +def _(person: Person): + static_assert(has_member(person, "name")) + static_assert(has_member(person, "age")) + + static_assert(has_member(person, "__dataclass_fields__")) + + static_assert(has_member(person, "__init__")) + static_assert(has_member(person, "__repr__")) + static_assert(has_member(person, "__eq__")) + static_assert(has_member(person, "__ne__")) + + static_assert(not has_member(person, "__lt__")) + static_assert(not has_member(person, "__le__")) + static_assert(not has_member(person, "__gt__")) + static_assert(not has_member(person, "__ge__")) + + static_assert(not has_member(person, "__slots__")) + + static_assert(not has_member(person, "__replace__")) +``` + +#### `__init__`, `__repr__` and `__eq__` + +`__init__`, `__repr__` and `__eq__` are always available (via `object`), even when `init=False`, +`repr=False` and `eq=False` are set: + +```py +from ty_extensions import has_member, static_assert +from dataclasses import dataclass + +@dataclass(init=False, repr=False, eq=False) +class C: + x: int + +static_assert(has_member(C, "__init__")) +static_assert(has_member(C, "__repr__")) +static_assert(has_member(C, "__eq__")) +static_assert(has_member(C, "__ne__")) +static_assert(has_member(C(), "__init__")) +static_assert(has_member(C(), "__repr__")) +static_assert(has_member(C(), "__eq__")) +static_assert(has_member(C(), "__ne__")) +``` + +#### `order=True` + +When `order=True` is set, comparison dunder methods become available: + +```py +from ty_extensions import has_member, static_assert +from dataclasses import dataclass + +@dataclass(order=True) +class C: + x: int + +static_assert(has_member(C, "__lt__")) +static_assert(has_member(C, "__le__")) +static_assert(has_member(C, "__gt__")) +static_assert(has_member(C, "__ge__")) + +def _(c: C): + static_assert(has_member(c, "__lt__")) + static_assert(has_member(c, "__le__")) + static_assert(has_member(c, "__gt__")) + static_assert(has_member(c, "__ge__")) +``` + +#### `slots=True` + +When `slots=True`, the corresponding dunder attribute becomes available: + +```py +from ty_extensions import has_member, static_assert +from dataclasses import dataclass + +@dataclass(slots=True) +class C: + x: int + +static_assert(has_member(C, "__slots__")) +static_assert(has_member(C(1), "__slots__")) +``` + +#### `__replace__` in Python 3.13+ + +Since Python 3.13, dataclasses have a `__replace__` method: + +```toml +[environment] +python-version = "3.13" +``` + +```py +from ty_extensions import has_member, static_assert +from dataclasses import dataclass + +@dataclass +class C: + x: int + +static_assert(has_member(C, "__replace__")) + +def _(c: C): + static_assert(has_member(c, "__replace__")) +``` + +#### `__match_args__` + +Since Python 3.10, dataclasses have a `__match_args__` attribute: + +```toml +[environment] +python-version = "3.10" +``` + +```py +from ty_extensions import has_member, static_assert +from dataclasses import dataclass + +@dataclass +class C: + x: int + +# TODO: add support for `__match_args__`: +static_assert(has_member(C, "__match_args__")) # error: [static-assert-error] -# TODO: this should ideally be available: -static_assert(has_member(Person, "__lt__")) # error: [static-assert-error] +def _(c: C): + # TODO: add support for `__match_args__`: + static_assert(has_member(c, "__match_args__")) # error: [static-assert-error] ``` ### Attributes not available at runtime diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 475c3017c7b6fc..554688d329df97 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -11,6 +11,7 @@ use crate::semantic_index::{ attribute_scopes, global_scope, place_table, semantic_index, use_def_map, }; use crate::types::call::{CallArguments, MatchedArgument}; +use crate::types::generics::Specialization; use crate::types::signatures::Signature; use crate::types::{CallDunderError, UnionType}; use crate::types::{ @@ -28,6 +29,21 @@ use rustc_hash::FxHashSet; pub use resolve_definition::{ImportAliasResolution, ResolvedDefinition, map_stub_definition}; use resolve_definition::{find_symbol_in_scope, resolve_definition}; +// `__init__`, `__repr__`, `__eq__`, `__ne__` and `__hash__` are always included via `object`, +// so we don't need to list them here. +const SYNTHETIC_DATACLASS_ATTRIBUTES: &[&str] = &[ + "__lt__", + "__le__", + "__gt__", + "__ge__", + "__replace__", + "__setattr__", + "__delattr__", + "__slots__", + "__weakref__", + "__dataclass_fields__", +]; + pub(crate) fn all_declarations_and_bindings<'db>( db: &'db dyn Db, scope_id: ScopeId<'db>, @@ -119,13 +135,9 @@ impl<'db> AllMembers<'db> { ), Type::NominalInstance(instance) => { - let class_literal = instance.class_literal(db); + let (class_literal, specialization) = instance.class(db).class_literal(db); self.extend_with_instance_members(db, ty, class_literal); - - // If this is a NamedTuple instance, include members from NamedTupleFallback - if CodeGeneratorKind::NamedTuple.matches(db, class_literal, None) { - self.extend_with_type(db, KnownClass::NamedTupleFallback.to_class_literal(db)); - } + self.extend_with_synthetic_members(db, ty, class_literal, specialization); } Type::NewTypeInstance(newtype) => { @@ -146,10 +158,7 @@ impl<'db> AllMembers<'db> { Type::ClassLiteral(class_literal) => { self.extend_with_class_members(db, ty, class_literal); - - if CodeGeneratorKind::NamedTuple.matches(db, class_literal, None) { - self.extend_with_type(db, KnownClass::NamedTupleFallback.to_class_literal(db)); - } + self.extend_with_synthetic_members(db, ty, class_literal, None); if let Type::ClassLiteral(meta_class_literal) = ty.to_meta_type(db) { self.extend_with_class_members(db, ty, meta_class_literal); @@ -158,23 +167,15 @@ impl<'db> AllMembers<'db> { Type::GenericAlias(generic_alias) => { let class_literal = generic_alias.origin(db); - if CodeGeneratorKind::NamedTuple.matches(db, class_literal, None) { - self.extend_with_type(db, KnownClass::NamedTupleFallback.to_class_literal(db)); - } self.extend_with_class_members(db, ty, class_literal); + self.extend_with_synthetic_members(db, ty, class_literal, None); } Type::SubclassOf(subclass_of_type) => { if let Some(class_type) = subclass_of_type.subclass_of().into_class() { - let class_literal = class_type.class_literal(db).0; + let (class_literal, specialization) = class_type.class_literal(db); self.extend_with_class_members(db, ty, class_literal); - - if CodeGeneratorKind::NamedTuple.matches(db, class_literal, None) { - self.extend_with_type( - db, - KnownClass::NamedTupleFallback.to_class_literal(db), - ); - } + self.extend_with_synthetic_members(db, ty, class_literal, specialization); } } @@ -414,6 +415,36 @@ impl<'db> AllMembers<'db> { } } } + + fn extend_with_synthetic_members( + &mut self, + db: &'db dyn Db, + ty: Type<'db>, + class_literal: ClassLiteral<'db>, + specialization: Option>, + ) { + match CodeGeneratorKind::from_class(db, class_literal, specialization) { + Some(CodeGeneratorKind::NamedTuple) => { + if ty.is_nominal_instance() { + self.extend_with_type(db, KnownClass::NamedTupleFallback.to_instance(db)); + } else { + self.extend_with_type(db, KnownClass::NamedTupleFallback.to_class_literal(db)); + } + } + Some(CodeGeneratorKind::TypedDict) => {} + Some(CodeGeneratorKind::DataclassLike(_)) => { + for attr in SYNTHETIC_DATACLASS_ATTRIBUTES { + if let Place::Defined(synthetic_member, _, _) = ty.member(db, attr).place { + self.members.insert(Member { + name: Name::from(*attr), + ty: synthetic_member, + }); + } + } + } + None => {} + } + } } /// A member of a type with an optional definition. diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 8c5adc9e0db266..8f0719fbc68817 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -88,6 +88,10 @@ impl<'db> Type<'db> { Type::NominalInstance(NominalInstanceType(NominalInstanceInner::ExactTuple(tuple))) } + pub(crate) const fn is_nominal_instance(self) -> bool { + matches!(self, Type::NominalInstance(_)) + } + pub(crate) const fn as_nominal_instance(self) -> Option> { match self { Type::NominalInstance(instance_type) => Some(instance_type), From ed256567b34dced7953b8d9bfd6f8b2494a16b6b Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 14 Nov 2025 10:15:46 +0100 Subject: [PATCH 2/6] Add __dataclass_params__ --- .../mdtest/ide_support/all_members.md | 2 ++ crates/ty_python_semantic/src/types/class.rs | 31 ++++++++++++------- .../src/types/ide_support.rs | 2 ++ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md index 196cb59e75d4fd..881e9ed0850a5e 100644 --- a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md +++ b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md @@ -570,6 +570,7 @@ static_assert(has_member(Person, "name")) static_assert(has_member(Person, "age")) static_assert(has_member(Person, "__dataclass_fields__")) +static_assert(has_member(Person, "__dataclass_params__")) # These are always available, since they are also defined on `object`: static_assert(has_member(Person, "__init__")) @@ -599,6 +600,7 @@ def _(person: Person): static_assert(has_member(person, "age")) static_assert(has_member(person, "__dataclass_fields__")) + static_assert(has_member(person, "__dataclass_params__")) static_assert(has_member(person, "__init__")) static_assert(has_member(person, "__repr__")) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index d939dabb018078..749edbc1c1166c 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2122,18 +2122,25 @@ impl<'db> ClassLiteral<'db> { specialization: Option>, name: &str, ) -> Member<'db> { - if name == "__dataclass_fields__" && self.dataclass_params(db).is_some() { - // Make this class look like a subclass of the `DataClassInstance` protocol - return Member { - inner: Place::declared(KnownClass::Dict.to_specialized_instance( - db, - [ - KnownClass::Str.to_instance(db), - KnownClass::Field.to_specialized_instance(db, [Type::any()]), - ], - )) - .with_qualifiers(TypeQualifiers::CLASS_VAR), - }; + if self.dataclass_params(db).is_some() { + if name == "__dataclass_fields__" { + // Make this class look like a subclass of the `DataClassInstance` protocol + return Member { + inner: Place::declared(KnownClass::Dict.to_specialized_instance( + db, + [ + KnownClass::Str.to_instance(db), + KnownClass::Field.to_specialized_instance(db, [Type::any()]), + ], + )) + .with_qualifiers(TypeQualifiers::CLASS_VAR), + }; + } else if name == "__dataclass_params__" { + // There is no typeshed class for this. For now, we model it as `Any`. + return Member { + inner: Place::declared(Type::any()).with_qualifiers(TypeQualifiers::CLASS_VAR), + }; + } } if CodeGeneratorKind::NamedTuple.matches(db, self, specialization) { diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 554688d329df97..d57869825af628 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -41,7 +41,9 @@ const SYNTHETIC_DATACLASS_ATTRIBUTES: &[&str] = &[ "__delattr__", "__slots__", "__weakref__", + "__match_args__", "__dataclass_fields__", + "__dataclass_params__", ]; pub(crate) fn all_declarations_and_bindings<'db>( From 3cc7b939449fd86cc187e8f060a19240853a74f9 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 14 Nov 2025 10:49:03 +0100 Subject: [PATCH 3/6] Add support for __weakref__ --- .../resources/mdtest/dataclasses/dataclasses.md | 13 ++++++++++++- .../resources/mdtest/ide_support/all_members.md | 16 ++++++++++++++++ crates/ty_python_semantic/src/types/class.rs | 11 +++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index 854808530241b1..e30e5526b56181 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -623,7 +623,18 @@ reveal_type(B.__slots__) # revealed: tuple[Literal["x"], Literal["y"]] ### `weakref_slot` -To do +When a dataclass is defined with `weakref_slot=True`, the `__weakref__` attribute is generated. For now, +we do not attempt to infer a more precise type for it. + +```py +from dataclasses import dataclass + +@dataclass(slots=True, weakref_slot=True) +class C: + x: int + +reveal_type(C.__weakref__) # revealed: Any +``` ## `Final` fields diff --git a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md index 881e9ed0850a5e..f9b786b016f304 100644 --- a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md +++ b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md @@ -680,6 +680,22 @@ static_assert(has_member(C, "__slots__")) static_assert(has_member(C(1), "__slots__")) ``` +#### `weakref_slot=True` + +When `weakref_slot=True`, the corresponding dunder attribute becomes available: + +```py +from ty_extensions import has_member, static_assert +from dataclasses import dataclass + +@dataclass(slots=True, weakref_slot=True) +class C: + x: int + +static_assert(has_member(C, "__weakref__")) +static_assert(has_member(C(1), "__weakref__")) +``` + #### `__replace__` in Python 3.13+ Since Python 3.13, dataclasses have a `__replace__` method: diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 749edbc1c1166c..04843bdce0ac1d 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2375,6 +2375,17 @@ impl<'db> ClassLiteral<'db> { Some(CallableType::function_like(db, signature)) } + (CodeGeneratorKind::DataclassLike(_), "__weakref__") => { + if !has_dataclass_param(DataclassFlags::WEAKREF_SLOT) + || !has_dataclass_param(DataclassFlags::SLOTS) + { + return None; + } + + // This could probably be `weakref | None`, but it does not seem important enough to + // model it precisely. + Some(Type::any()) + } (CodeGeneratorKind::NamedTuple, name) if name != "__init__" => { KnownClass::NamedTupleFallback .to_class_literal(db) From 46ab0ed9c2c026b25cb8d20770c77a4b734647d1 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 14 Nov 2025 10:49:17 +0100 Subject: [PATCH 4/6] Add support for __match_args__ --- .../mdtest/dataclasses/dataclasses.md | 50 +++++++++++++++++-- .../mdtest/ide_support/all_members.md | 6 +-- crates/ty_python_semantic/src/types/class.rs | 20 ++++++++ 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index e30e5526b56181..fba034d9b92ea9 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -461,7 +461,51 @@ del frozen.x # TODO this should emit an [invalid-assignment] ### `match_args` -To do +If `match_args` is set to `True` (the default), the `__match_args__` attribute is a tuple created +from the list of non keyword-only parameters to the synthesized `__init__` method (even if +`__init__` is not actually generated). + +```py +from dataclasses import dataclass, field + +@dataclass +class WithMatchArgs: + normal_a: str + normal_b: int + kw_only: int = field(kw_only=True) + +reveal_type(WithMatchArgs.__match_args__) # revealed: tuple[Literal["normal_a"], Literal["normal_b"]] + +@dataclass(kw_only=True) +class KwOnlyDefaultMatchArgs: + normal_a: str = field(kw_only=False) + normal_b: int = field(kw_only=False) + kw_only: int + +reveal_type(KwOnlyDefaultMatchArgs.__match_args__) # revealed: tuple[Literal["normal_a"], Literal["normal_b"]] + +@dataclass(match_args=True) +class ExplicitMatchArgs: + normal: str + +reveal_type(ExplicitMatchArgs.__match_args__) # revealed: tuple[Literal["normal"]] + +@dataclass +class Empty: ... + +reveal_type(Empty.__match_args__) # revealed: tuple[()] +``` + +When `match_args` is explicitly set to `False`, the `__match_args__` attribute is not available: + +```py +@dataclass(match_args=False) +class NoMatchArgs: + x: int + y: str + +NoMatchArgs.__match_args__ # error: [unresolved-attribute] +``` ### `kw_only` @@ -623,8 +667,8 @@ reveal_type(B.__slots__) # revealed: tuple[Literal["x"], Literal["y"]] ### `weakref_slot` -When a dataclass is defined with `weakref_slot=True`, the `__weakref__` attribute is generated. For now, -we do not attempt to infer a more precise type for it. +When a dataclass is defined with `weakref_slot=True`, the `__weakref__` attribute is generated. For +now, we do not attempt to infer a more precise type for it. ```py from dataclasses import dataclass diff --git a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md index f9b786b016f304..0ef6eda4a06f00 100644 --- a/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md +++ b/crates/ty_python_semantic/resources/mdtest/ide_support/all_members.md @@ -736,12 +736,10 @@ from dataclasses import dataclass class C: x: int -# TODO: add support for `__match_args__`: -static_assert(has_member(C, "__match_args__")) # error: [static-assert-error] +static_assert(has_member(C, "__match_args__")) def _(c: C): - # TODO: add support for `__match_args__`: - static_assert(has_member(c, "__match_args__")) # error: [static-assert-error] + static_assert(has_member(c, "__match_args__")) ``` ### Attributes not available at runtime diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 04843bdce0ac1d..ee5c106c469f25 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2375,6 +2375,26 @@ impl<'db> ClassLiteral<'db> { Some(CallableType::function_like(db, signature)) } + (CodeGeneratorKind::DataclassLike(_), "__match_args__") => { + if !has_dataclass_param(DataclassFlags::MATCH_ARGS) { + return None; + } + + let kw_only_default = has_dataclass_param(DataclassFlags::KW_ONLY); + + let fields = self.fields(db, specialization, field_policy); + let match_args = fields + .iter() + .filter(|(_, field)| { + if let FieldKind::Dataclass { init, kw_only, .. } = &field.kind { + *init && !kw_only.unwrap_or(kw_only_default) + } else { + false + } + }) + .map(|(name, _)| Type::string_literal(db, name)); + Some(Type::heterogeneous_tuple(db, match_args)) + } (CodeGeneratorKind::DataclassLike(_), "__weakref__") => { if !has_dataclass_param(DataclassFlags::WEAKREF_SLOT) || !has_dataclass_param(DataclassFlags::SLOTS) From 19b5899c01678866d496d11586e845fbd3428872 Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 14 Nov 2025 10:59:30 +0100 Subject: [PATCH 5/6] Any | None --- .../resources/mdtest/dataclasses/dataclasses.md | 2 +- crates/ty_python_semantic/src/types/class.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index fba034d9b92ea9..906e79c7b90946 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -677,7 +677,7 @@ from dataclasses import dataclass class C: x: int -reveal_type(C.__weakref__) # revealed: Any +reveal_type(C.__weakref__) # revealed: Any | None ``` ## `Final` fields diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index ee5c106c469f25..a720f887ad02a8 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2404,7 +2404,7 @@ impl<'db> ClassLiteral<'db> { // This could probably be `weakref | None`, but it does not seem important enough to // model it precisely. - Some(Type::any()) + Some(UnionType::from_elements(db, [Type::any(), Type::none(db)])) } (CodeGeneratorKind::NamedTuple, name) if name != "__init__" => { KnownClass::NamedTupleFallback From 6059145a5b860e35751e08ac63ba4c2817c512ec Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 14 Nov 2025 11:00:51 +0100 Subject: [PATCH 6/6] __match_args__ is only available on 3.10+ --- crates/ty_python_semantic/src/types/class.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index a720f887ad02a8..809c5e7259ffe9 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2375,7 +2375,9 @@ impl<'db> ClassLiteral<'db> { Some(CallableType::function_like(db, signature)) } - (CodeGeneratorKind::DataclassLike(_), "__match_args__") => { + (CodeGeneratorKind::DataclassLike(_), "__match_args__") + if Program::get(db).python_version(db) >= PythonVersion::PY310 => + { if !has_dataclass_param(DataclassFlags::MATCH_ARGS) { return None; }