From 238a8f669f7d2926db9f9bf454d7b5e6a3948a52 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 28 Nov 2025 14:27:05 -0800 Subject: [PATCH 1/4] Fix crash involving Unpack-ed TypeVarTuple Fixes #20093 --- mypy/typeops.py | 6 +++-- mypy/types.py | 31 +++++++++++++++++-------- test-data/unit/check-overloading.test | 13 +++++++++++ test-data/unit/check-typevar-tuple.test | 22 ++++++++++++++++++ 4 files changed, 60 insertions(+), 12 deletions(-) diff --git a/mypy/typeops.py b/mypy/typeops.py index 050252eb6205..ad4d5d7407d3 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -508,7 +508,7 @@ def erase_to_bound(t: Type) -> Type: def callable_corresponding_argument( typ: NormalizedCallableType | Parameters, model: FormalArgument ) -> FormalArgument | None: - """Return the argument a function that corresponds to `model`""" + """Return the argument of a function that corresponds to `model`""" by_name = typ.argument_by_name(model.name) by_pos = typ.argument_by_position(model.pos) @@ -522,7 +522,7 @@ def callable_corresponding_argument( # taking both *args and **args, or a pair of functions like so: # def right(a: int = ...) -> None: ... - # def left(__a: int = ..., *, a: int = ...) -> None: ... + # def left(x: int = ..., /, *, a: int = ...) -> None: ... from mypy.meet import meet_types if ( @@ -533,6 +533,8 @@ def callable_corresponding_argument( return FormalArgument( by_name.name, by_pos.pos, meet_types(by_name.typ, by_pos.typ), False ) + return by_name + return by_name if by_name is not None else by_pos diff --git a/mypy/types.py b/mypy/types.py index 09e4b74bb821..d4edcc86e303 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1923,6 +1923,25 @@ def __hash__(self) -> int: return hash((self.name, self.pos, self.typ, self.required)) +def _synthesize_arg_from_vararg(vararg: FormalArgument | None, position: int | None) -> FormalArgument | None: + if vararg is None: + return None + typ = vararg.typ + if isinstance(typ, UnpackType): + # Similar to logic in ExpressionChecker.visit_tuple_index_helper + unpacked = get_proper_type(typ.type) + if isinstance(unpacked, TypeVarTupleType): + bound = get_proper_type(unpacked.upper_bound) + assert isinstance(bound, Instance) + assert bound.type.fullname == "builtins.tuple" + typ = bound.args[0] + else: + assert isinstance(unpacked, Instance) + assert unpacked.type.fullname == "builtins.tuple" + typ = unpacked.args[0] + return FormalArgument(None, position, typ, False) + + class Parameters(ProperType): """Type that represents the parameters to a function. @@ -2069,11 +2088,7 @@ def try_synthesizing_arg_from_kwarg(self, name: str | None) -> FormalArgument | return None def try_synthesizing_arg_from_vararg(self, position: int | None) -> FormalArgument | None: - var_arg = self.var_arg() - if var_arg is not None: - return FormalArgument(None, position, var_arg.typ, False) - else: - return None + return _synthesize_arg_from_vararg(self.var_arg(), position) def accept(self, visitor: TypeVisitor[T]) -> T: return visitor.visit_parameters(self) @@ -2418,11 +2433,7 @@ def try_synthesizing_arg_from_kwarg(self, name: str | None) -> FormalArgument | return None def try_synthesizing_arg_from_vararg(self, position: int | None) -> FormalArgument | None: - var_arg = self.var_arg() - if var_arg is not None: - return FormalArgument(None, position, var_arg.typ, False) - else: - return None + return _synthesize_arg_from_vararg(self.var_arg(), position) @property def items(self) -> list[CallableType]: diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test index be55a182b87b..1830a0c5ce3c 100644 --- a/test-data/unit/check-overloading.test +++ b/test-data/unit/check-overloading.test @@ -263,6 +263,19 @@ def foo(*args: int | str, **kw: int | Foo) -> None: pass [builtins fixtures/tuple.pyi] + +[case testTypeCheckOverloadImplOverlapVarArgsAndKwargsNever] +from __future__ import annotations +from typing import overload + +@overload # E: Single overload definition, multiple required +def foo(x: int) -> None: ... + +def foo(*args: int, **kw: str) -> None: # E: Overloaded function implementation does not accept all possible arguments of signature 1 + pass +[builtins fixtures/tuple.pyi] + + [case testTypeCheckOverloadWithImplTooSpecificRetType] from typing import overload, Any diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index cb5029ee4e6d..35675406b379 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2716,3 +2716,25 @@ class MyTuple(tuple[Unpack[Union[int, str]]], Generic[Unpack[Ts]]): # E: "Union x: MyTuple[int, str] reveal_type(x[0]) # N: Revealed type is "Any" [builtins fixtures/tuple.pyi] + +[case testHigherOrderFunctionUnpackTypeVarTupleViaParamSpec] +from typing import Callable, ParamSpec, TypeVar, TypeVarTuple, Unpack + +P = ParamSpec("P") +T = TypeVar("T") +Ts = TypeVarTuple("Ts") + +def call(func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T: + return func(*args, **kwargs) + + +def run(func: Callable[[Unpack[Ts]], T], *args: Unpack[Ts], some_kwarg: str = "asdf") -> T: + raise + + +def foo() -> str: + return "hello" + + +call(run, foo, some_kwarg="a") # E: Argument 1 to "call" has incompatible type "def [Ts`-1, T] run(func: def (*Unpack[Ts]) -> T, *args: Unpack[Ts], some_kwarg: str = ...) -> T"; expected "Callable[[Callable[[], str], str], str]" +[builtins fixtures/tuple.pyi] From 2286cb700b4710e7fcd2b9ab63d8098922a2226d Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 28 Nov 2025 14:30:25 -0800 Subject: [PATCH 2/4] . --- test-data/unit/check-typevar-tuple.test | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 35675406b379..c60d0aec0835 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2736,5 +2736,6 @@ def foo() -> str: return "hello" +# this is a false positive, but it no longer crashes call(run, foo, some_kwarg="a") # E: Argument 1 to "call" has incompatible type "def [Ts`-1, T] run(func: def (*Unpack[Ts]) -> T, *args: Unpack[Ts], some_kwarg: str = ...) -> T"; expected "Callable[[Callable[[], str], str], str]" [builtins fixtures/tuple.pyi] From 9c1d80d565b9ea9ea64120b424f3789e22088ae6 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 28 Nov 2025 14:34:21 -0800 Subject: [PATCH 3/4] . --- mypy/types.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/types.py b/mypy/types.py index d4edcc86e303..51c9c24e80d1 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1923,7 +1923,9 @@ def __hash__(self) -> int: return hash((self.name, self.pos, self.typ, self.required)) -def _synthesize_arg_from_vararg(vararg: FormalArgument | None, position: int | None) -> FormalArgument | None: +def _synthesize_arg_from_vararg( + vararg: FormalArgument | None, position: int | None +) -> FormalArgument | None: if vararg is None: return None typ = vararg.typ From 29d17c1b29a17666692e5be42e82d45a52e7176a Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Fri, 28 Nov 2025 16:10:52 -0800 Subject: [PATCH 4/4] less ambitious --- mypy/typeops.py | 4 ++++ mypy/types.py | 33 ++++++++++----------------------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/mypy/typeops.py b/mypy/typeops.py index ad4d5d7407d3..f6646740031d 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -529,6 +529,10 @@ def callable_corresponding_argument( not (by_name.required or by_pos.required) and by_pos.name is None and by_name.pos is None + # This is not principled, but prevents a crash. It's weird to have a FormalArgument + # that has an UnpackType. + and not isinstance(by_name.typ, UnpackType) + and not isinstance(by_pos.typ, UnpackType) ): return FormalArgument( by_name.name, by_pos.pos, meet_types(by_name.typ, by_pos.typ), False diff --git a/mypy/types.py b/mypy/types.py index 51c9c24e80d1..09e4b74bb821 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1923,27 +1923,6 @@ def __hash__(self) -> int: return hash((self.name, self.pos, self.typ, self.required)) -def _synthesize_arg_from_vararg( - vararg: FormalArgument | None, position: int | None -) -> FormalArgument | None: - if vararg is None: - return None - typ = vararg.typ - if isinstance(typ, UnpackType): - # Similar to logic in ExpressionChecker.visit_tuple_index_helper - unpacked = get_proper_type(typ.type) - if isinstance(unpacked, TypeVarTupleType): - bound = get_proper_type(unpacked.upper_bound) - assert isinstance(bound, Instance) - assert bound.type.fullname == "builtins.tuple" - typ = bound.args[0] - else: - assert isinstance(unpacked, Instance) - assert unpacked.type.fullname == "builtins.tuple" - typ = unpacked.args[0] - return FormalArgument(None, position, typ, False) - - class Parameters(ProperType): """Type that represents the parameters to a function. @@ -2090,7 +2069,11 @@ def try_synthesizing_arg_from_kwarg(self, name: str | None) -> FormalArgument | return None def try_synthesizing_arg_from_vararg(self, position: int | None) -> FormalArgument | None: - return _synthesize_arg_from_vararg(self.var_arg(), position) + var_arg = self.var_arg() + if var_arg is not None: + return FormalArgument(None, position, var_arg.typ, False) + else: + return None def accept(self, visitor: TypeVisitor[T]) -> T: return visitor.visit_parameters(self) @@ -2435,7 +2418,11 @@ def try_synthesizing_arg_from_kwarg(self, name: str | None) -> FormalArgument | return None def try_synthesizing_arg_from_vararg(self, position: int | None) -> FormalArgument | None: - return _synthesize_arg_from_vararg(self.var_arg(), position) + var_arg = self.var_arg() + if var_arg is not None: + return FormalArgument(None, position, var_arg.typ, False) + else: + return None @property def items(self) -> list[CallableType]: