From d37f5ab73662efcdeb127200f94645fa15c7df63 Mon Sep 17 00:00:00 2001 From: bahtya Date: Thu, 9 Apr 2026 18:51:32 +0800 Subject: [PATCH 1/2] fix(checker): avoid false overload-cannot-match with ParamSpec args When an overload uses ParamSpec-flavored *args (P.args) or **kwargs (P.kwargs), erasing the ParamSpec to Any makes the signature appear to accept all arguments. This causes a false 'overload will never be matched' error when combined with a second overload that has explicit keyword-only parameters. Skip the can-never-match check when the first overload has ParamSpec-flavored variadic arguments, since we cannot reliably determine overlap after erasure. Fixes #21171 Signed-off-by: bahtya --- mypy/checker.py | 11 +++++++++++ .../unit/check-parameter-specification.test | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 8775f1ddef294..54b3c9d388836 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -228,6 +228,7 @@ TypeVarId, TypeVarLikeType, TypeVarTupleType, + ParamSpecType, TypeVarType, UnboundType, UninhabitedType, @@ -8954,6 +8955,16 @@ def overload_can_never_match(signature: CallableType, other: CallableType) -> bo Assumes that both signatures have overlapping argument counts. """ + # If the signature uses ParamSpec-flavored *args or **kwargs, we cannot + # reliably determine overlap. Erasing a ParamSpec to Any makes + # P.args/P.kwargs look like *Any/**Any, which appears to accept all + # arguments — but in reality the ParamSpec is constrained to the + # wrapped function's parameters. This leads to false positives where + # we incorrectly conclude that the other overload can never match. + for arg_type in signature.arg_types: + if isinstance(arg_type, ParamSpecType) and arg_type.flavor != 0: # BARE = 0 + return False + # The extra erasure is needed to prevent spurious errors # in situations where an `Any` overload is used as a fallback # for an overload with type variables. The spurious error appears diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index b0808105a3858..d105832f1fa94 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -2756,3 +2756,19 @@ reveal_type(Sneaky(f8, 1, y='').kwargs) # N: Revealed type is "builtins.dict[bu reveal_type(Sneaky(f9, 1, y=0).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y': builtins.int, 'z'?: builtins.str})" reveal_type(Sneaky(f9, 1, y=0, z='').kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y': builtins.int, 'z'?: builtins.str})" [builtins fixtures/paramspec.pyi] + +[case testOverloadParamSpecNoFalsePositiveCannotMatch] +# Test that overloads using P.args/P.kwargs don't trigger false +# "overload will never be matched" errors when combined with +# overloads using explicit keyword-only parameters. +from typing import Any, overload, ParamSpec, TypeVar, Callable + +P = ParamSpec("P") +T = TypeVar("T") + +@overload +def bar(f: Callable[P, T], *a: P.args, **k: P.kwargs) -> T: ... +@overload +def bar(f: Callable[..., T], *a: Any, baz: int, **k: Any) -> T: ... +def bar(f, *a, **k): ... +[builtins fixtures/paramspec.pyi] From a140227868503dc9e23c9e1f5d8ccd0018868048 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:53:47 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 54b3c9d388836..7b91a8a3b0180 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -215,6 +215,7 @@ LiteralType, NoneType, Overloaded, + ParamSpecType, PartialType, ProperType, TupleType, @@ -228,7 +229,6 @@ TypeVarId, TypeVarLikeType, TypeVarTupleType, - ParamSpecType, TypeVarType, UnboundType, UninhabitedType,