From ffd5016416aee1be58cf653985a9931ce38fcdda Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Wed, 21 Oct 2026 07:29:34 -0700 Subject: [PATCH] Fix false positive on generic NamedTuple `__new__` Use erased named-tuple items when filling type variables with Any fill_typevars_with_any computes an erased tuple but copies the original items back into its result. Keep the erased items so they agree with the Any-parameterized fallback, and remove the caller's compensating erasure. This also prevents false __new__ return-type errors for generic named-tuple subclasses. Extend the constructor test to accept a concrete specialization while still rejecting a return type outside the subclass. Authored by Codex --- mypy/checker.py | 8 +------- mypy/typevars.py | 2 +- test-data/unit/check-namedtuple.test | 14 +++++++++++++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 64852cb724f43..386f2a31d3f17 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8463,13 +8463,7 @@ def get_type_range_of_type(self, typ: Type) -> TypeRange | None: return TypeRange(item, is_upper_bound=True) if isinstance(typ, FunctionLike) and typ.is_type_obj(): # If a type is generic, `isinstance` can only narrow its variables to Any. - any_parameterized = fill_typevars_with_any(typ.type_object()) - # Tuples may have unattended type variables among their items - if isinstance(any_parameterized, TupleType): - erased_type = erase_typevars(any_parameterized) - else: - erased_type = any_parameterized - return TypeRange(erased_type, is_upper_bound=False) + return TypeRange(fill_typevars_with_any(typ.type_object()), is_upper_bound=False) if isinstance(typ, TypeType): # Type[A] means "any type that is a subtype of A" rather than "precisely type A" # we indicate this by setting is_upper_bound flag diff --git a/mypy/typevars.py b/mypy/typevars.py index e871973104a2d..807ce8acb12cc 100644 --- a/mypy/typevars.py +++ b/mypy/typevars.py @@ -70,7 +70,7 @@ def fill_typevars_with_any(typ: TypeInfo) -> Instance | TupleType: erased_tuple_type = erase_typevars(typ.tuple_type, {tv.id for tv in typ.defn.type_vars}) assert isinstance(erased_tuple_type, ProperType) if isinstance(erased_tuple_type, TupleType): - return typ.tuple_type.copy_modified(fallback=inst) + return erased_tuple_type.copy_modified(fallback=inst) return inst diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index 1ed57c72fcd23..9573e66da4c78 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -875,7 +875,7 @@ def f(o: Base) -> None: [out] [case testNamedTupleNew] -from typing import NamedTuple +from typing import Generic, NamedTuple, TypeVar Base = NamedTuple('Base', [('param', int)]) @@ -885,6 +885,18 @@ class Child(Base): Base(param=10) Child(param=10) + +T = TypeVar("T") +class GenericBase(NamedTuple, Generic[T]): + value: T + +class GenericChild(GenericBase[T]): + def __new__(cls, value: int) -> GenericChild[int]: + raise BaseException + +class BadChild(GenericBase[T]): + def __new__(cls, value: int) -> GenericBase[int]: # E: Incompatible return type for "__new__" (returns "GenericBase[int]", but must return a subtype of "BadChild[Any]") + raise BaseException [builtins fixtures/tuple.pyi] [case testNamedTupleClassMethodWithGenericReturnValue]