From 5c97b6ef40cd7fb34c2c69619657ef150d6031f4 Mon Sep 17 00:00:00 2001 From: Dex Devlon <51504045+bxff@users.noreply.github.com> Date: Sat, 17 Jan 2026 00:26:56 +0530 Subject: [PATCH] [ty] Correct return type for synthesized NamedTuple.__new__ methods (#22625) --- .../resources/mdtest/named_tuple.md | 50 +++++++++++++++++-- crates/ty_python_semantic/src/types/class.rs | 17 +++++-- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index 42602a2327..0f50fbbe25 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -256,7 +256,7 @@ class Url(NamedTuple("Url", [("host", str), ("path", str)])): reveal_type(Url) # revealed: # revealed: (, , , ) reveal_mro(Url) -reveal_type(Url.__new__) # revealed: (cls: type, host: str, path: str) -> Url +reveal_type(Url.__new__) # revealed: [Self](cls: type[Self], host: str, path: str) -> Self # Constructor works with the inherited fields. url = Url("example.com", "/path") @@ -451,7 +451,7 @@ from ty_extensions import reveal_mro # `rename=True` replaces invalid identifiers with positional names Point = collections.namedtuple("Point", ["x", "class", "_y", "z", "z"], rename=True) reveal_type(Point) # revealed: -reveal_type(Point.__new__) # revealed: (cls: type, x: Any, _1: Any, _2: Any, z: Any, _4: Any) -> Point +reveal_type(Point.__new__) # revealed: [Self](cls: type[Self], x: Any, _1: Any, _2: Any, z: Any, _4: Any) -> Self reveal_mro(Point) # revealed: (, , ) p = Point(1, 2, 3, 4, 5) reveal_type(p.x) # revealed: Any @@ -464,7 +464,7 @@ reveal_type(p._4) # revealed: Any # error: [invalid-argument-type] "Invalid argument to parameter `rename` of `namedtuple()`" Point2 = collections.namedtuple("Point2", ["_x", "class"], rename=1) reveal_type(Point2) # revealed: -reveal_type(Point2.__new__) # revealed: (cls: type, _0: Any, _1: Any) -> Point2 +reveal_type(Point2.__new__) # revealed: [Self](cls: type[Self], _0: Any, _1: Any) -> Self # Without `rename=True`, invalid field names emit diagnostics: # - Field names starting with underscore @@ -490,7 +490,7 @@ reveal_type(Invalid) # revealed: # `defaults` provides default values for the rightmost fields Person = collections.namedtuple("Person", ["name", "age", "city"], defaults=["Unknown"]) reveal_type(Person) # revealed: -reveal_type(Person.__new__) # revealed: (cls: type, name: Any, age: Any, city: Any = "Unknown") -> Person +reveal_type(Person.__new__) # revealed: [Self](cls: type[Self], name: Any, age: Any, city: Any = "Unknown") -> Self reveal_mro(Person) # revealed: (, , ) # Can create with all fields @@ -508,7 +508,7 @@ reveal_type(Config) # revealed: # error: [invalid-named-tuple] "Too many defaults for `namedtuple()`" TooManyDefaults = collections.namedtuple("TooManyDefaults", ["x", "y"], defaults=("a", "b", "c")) reveal_type(TooManyDefaults) # revealed: -reveal_type(TooManyDefaults.__new__) # revealed: (cls: type, x: Any = "a", y: Any = "b") -> TooManyDefaults +reveal_type(TooManyDefaults.__new__) # revealed: [Self](cls: type[Self], x: Any = "a", y: Any = "b") -> Self # Unknown keyword arguments produce an error # error: [unknown-argument] @@ -1391,3 +1391,43 @@ class Foo(NamedTuple): # error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`" _asdict = True ``` + +## `super().__new__` in `NamedTuple` subclasses + +This is a regression test for . + +```py +from typing import NamedTuple, Generic, TypeVar +from typing_extensions import Self + +class Base(NamedTuple): + x: int + y: int + +class Child(Base): + def __new__(cls, x: int, y: int) -> Self: + instance = super().__new__(cls, x, y) + reveal_type(instance) # revealed: Self@__new__ + return instance + +reveal_type(Child(1, 2)) # revealed: Child + +T = TypeVar("T") + +class GenericBase(NamedTuple, Generic[T]): + x: T + +class ConcreteChild(GenericBase[str]): + def __new__(cls, x: str) -> "ConcreteChild": + instance = super().__new__(cls, x) + reveal_type(instance) # revealed: Self@__new__ + return instance + +class GenericChild(GenericBase[T]): + def __new__(cls, x: T) -> Self: + instance = super().__new__(cls, x) + reveal_type(instance) # revealed: @Todo(super in generic class) + return instance + +reveal_type(GenericChild(x=3.14)) # revealed: GenericChild[int | float] +``` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 71e1a6edec..97d5553bfb 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -5204,9 +5204,20 @@ fn synthesize_namedtuple_class_member<'db>( match name { "__new__" => { // __new__(cls, field1, field2, ...) -> Self + let self_typevar = + BoundTypeVarInstance::synthetic_self(db, instance_ty, BindingContext::Synthetic); + let self_ty = Type::TypeVar(self_typevar); + + let variables = inherited_generic_context + .iter() + .flat_map(|ctx| ctx.variables(db)) + .chain(std::iter::once(self_typevar)); + + let generic_context = GenericContext::from_typevar_instances(db, variables); + let mut parameters = vec![ Parameter::positional_or_keyword(Name::new_static("cls")) - .with_annotated_type(KnownClass::Type.to_instance(db)), + .with_annotated_type(SubclassOfType::from(db, self_typevar)), ]; for (field_name, field_ty, default_ty) in fields { @@ -5219,9 +5230,9 @@ fn synthesize_namedtuple_class_member<'db>( } let signature = Signature::new_generic( - inherited_generic_context, + Some(generic_context), Parameters::new(db, parameters), - instance_ty, + self_ty, ); Some(Type::function_like_callable(db, signature)) }