From dff33a1d296538cd7cfd046317aab31740b59ab0 Mon Sep 17 00:00:00 2001 From: cyphercodes Date: Tue, 5 May 2026 18:23:45 +0300 Subject: [PATCH 1/5] Fix nested Concatenate with ellipsis crash Fixes #21404. --- mypy/typeanal.py | 3 +++ test-data/unit/check-parameter-specification.test | 1 + 2 files changed, 4 insertions(+) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index db56256192625..9ece662e97b2a 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -580,6 +580,9 @@ def apply_concatenate_operator(self, t: UnboundType) -> Type: self.api.fail("Nested Concatenates are invalid", t, code=codes.VALID_TYPE) args = self.anal_array(t.args[:-1]) + if any(isinstance(arg, (Parameters, ParamSpecType)) for arg in args): + self.api.fail("Nested Concatenates are invalid", t, code=codes.VALID_TYPE) + return AnyType(TypeOfAny.from_error) pre = ps.prefix if isinstance(ps, ParamSpecType) else ps # mypy can't infer this :( diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 970ba45d0e8e2..294790ef330a2 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -1147,6 +1147,7 @@ from typing import Callable P = ParamSpec("P") def x(f: Callable[Concatenate[int, Concatenate[int, P]], None]) -> None: ... # E: Nested Concatenates are invalid +def y(f: Callable[Concatenate[Concatenate[...], ...], None]) -> None: ... # E: Nested Concatenates are invalid [builtins fixtures/paramspec.pyi] [case testPropagatedAnyConstraintsAreOK] From 6ae296ac668e8eb72cb66da05818a998e4d7494f Mon Sep 17 00:00:00 2001 From: cyphercodes Date: Thu, 7 May 2026 11:51:28 +0300 Subject: [PATCH 2/5] fix: reject nested Concatenate during type analysis --- mypy/typeanal.py | 40 +++++++++---------- .../unit/check-parameter-specification.test | 2 +- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 9ece662e97b2a..d611727e64443 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -580,9 +580,6 @@ def apply_concatenate_operator(self, t: UnboundType) -> Type: self.api.fail("Nested Concatenates are invalid", t, code=codes.VALID_TYPE) args = self.anal_array(t.args[:-1]) - if any(isinstance(arg, (Parameters, ParamSpecType)) for arg in args): - self.api.fail("Nested Concatenates are invalid", t, code=codes.VALID_TYPE) - return AnyType(TypeOfAny.from_error) pre = ps.prefix if isinstance(ps, ParamSpecType) else ps # mypy can't infer this :( @@ -1930,27 +1927,28 @@ def anal_type( self.allow_typed_dict_special_forms = old_allow_typed_dict_special_forms self.allow_ellipsis = old_allow_ellipsis self.allow_unpack = old_allow_unpack - if ( - not allow_param_spec - and isinstance(analyzed, ParamSpecType) - and analyzed.flavor == ParamSpecFlavor.BARE - ): - if analyzed.prefix.arg_types: + if not allow_param_spec: + if isinstance(analyzed, Parameters): self.fail("Invalid location for Concatenate", t, code=codes.VALID_TYPE) self.note("You can use Concatenate as the first argument to Callable", t) analyzed = AnyType(TypeOfAny.from_error) - else: - self.fail( - INVALID_PARAM_SPEC_LOCATION.format(format_type(analyzed, self.options)), - t, - code=codes.VALID_TYPE, - ) - self.note( - INVALID_PARAM_SPEC_LOCATION_NOTE.format(analyzed.name), - t, - code=codes.VALID_TYPE, - ) - analyzed = AnyType(TypeOfAny.from_error) + elif isinstance(analyzed, ParamSpecType) and analyzed.flavor == ParamSpecFlavor.BARE: + if analyzed.prefix.arg_types: + self.fail("Invalid location for Concatenate", t, code=codes.VALID_TYPE) + self.note("You can use Concatenate as the first argument to Callable", t) + analyzed = AnyType(TypeOfAny.from_error) + else: + self.fail( + INVALID_PARAM_SPEC_LOCATION.format(format_type(analyzed, self.options)), + t, + code=codes.VALID_TYPE, + ) + self.note( + INVALID_PARAM_SPEC_LOCATION_NOTE.format(analyzed.name), + t, + code=codes.VALID_TYPE, + ) + analyzed = AnyType(TypeOfAny.from_error) return analyzed def anal_var_def(self, var_def: TypeVarLikeType) -> TypeVarLikeType: diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 294790ef330a2..194795ebe9dce 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -1147,7 +1147,7 @@ from typing import Callable P = ParamSpec("P") def x(f: Callable[Concatenate[int, Concatenate[int, P]], None]) -> None: ... # E: Nested Concatenates are invalid -def y(f: Callable[Concatenate[Concatenate[...], ...], None]) -> None: ... # E: Nested Concatenates are invalid +def y(f: Callable[Concatenate[Concatenate[...], ...], None]) -> None: ... # E: Invalid location for Concatenate # N: You can use Concatenate as the first argument to Callable [builtins fixtures/paramspec.pyi] [case testPropagatedAnyConstraintsAreOK] From 2c23ef64ecaa9c0edc163a89b3a1d3df345a1105 Mon Sep 17 00:00:00 2001 From: cyphercodes Date: Thu, 7 May 2026 13:57:27 +0300 Subject: [PATCH 3/5] test: update tuple Concatenate expected output --- test-data/unit/check-tuples.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test index bfbd2e631f5d8..6568ba0b5f825 100644 --- a/test-data/unit/check-tuples.test +++ b/test-data/unit/check-tuples.test @@ -1835,6 +1835,6 @@ inst_tuple_aa_subclass: tuple_aa_subclass = tuple_aa_subclass((A(), A()))[:] # [case testTuplePassedParameters] from typing_extensions import Concatenate -def c(t: tuple[Concatenate[int, ...]]) -> None: # E: Cannot use "[int, VarArg(Any), KwArg(Any)]" for tuple, only for ParamSpec +def c(t: tuple[Concatenate[int, ...]]) -> None: # E: Invalid location for Concatenate # N: You can use Concatenate as the first argument to Callable reveal_type(t) # N: Revealed type is "tuple[Any]" [builtins fixtures/tuple.pyi] From f8c28ab1a38bd8bb6334c65d0e33db7d8bbf4bd0 Mon Sep 17 00:00:00 2001 From: cyphercodes Date: Thu, 7 May 2026 21:12:32 +0300 Subject: [PATCH 4/5] refactor: simplify Concatenate location errors --- mypy/typeanal.py | 39 ++++++++++++++++---------------- test-data/unit/check-tuples.test | 7 ------ 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index d611727e64443..12d7325313b8c 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1927,28 +1927,27 @@ def anal_type( self.allow_typed_dict_special_forms = old_allow_typed_dict_special_forms self.allow_ellipsis = old_allow_ellipsis self.allow_unpack = old_allow_unpack - if not allow_param_spec: - if isinstance(analyzed, Parameters): + if ( + not allow_param_spec + and isinstance(analyzed, (ParamSpecType, Parameters)) + and (isinstance(analyzed, Parameters) or analyzed.flavor == ParamSpecFlavor.BARE) + ): + is_concatenate = isinstance(analyzed, Parameters) or analyzed.prefix.arg_types + if is_concatenate: self.fail("Invalid location for Concatenate", t, code=codes.VALID_TYPE) self.note("You can use Concatenate as the first argument to Callable", t) - analyzed = AnyType(TypeOfAny.from_error) - elif isinstance(analyzed, ParamSpecType) and analyzed.flavor == ParamSpecFlavor.BARE: - if analyzed.prefix.arg_types: - self.fail("Invalid location for Concatenate", t, code=codes.VALID_TYPE) - self.note("You can use Concatenate as the first argument to Callable", t) - analyzed = AnyType(TypeOfAny.from_error) - else: - self.fail( - INVALID_PARAM_SPEC_LOCATION.format(format_type(analyzed, self.options)), - t, - code=codes.VALID_TYPE, - ) - self.note( - INVALID_PARAM_SPEC_LOCATION_NOTE.format(analyzed.name), - t, - code=codes.VALID_TYPE, - ) - analyzed = AnyType(TypeOfAny.from_error) + else: + self.fail( + INVALID_PARAM_SPEC_LOCATION.format(format_type(analyzed, self.options)), + t, + code=codes.VALID_TYPE, + ) + self.note( + INVALID_PARAM_SPEC_LOCATION_NOTE.format(analyzed.name), + t, + code=codes.VALID_TYPE, + ) + analyzed = AnyType(TypeOfAny.from_error) return analyzed def anal_var_def(self, var_def: TypeVarLikeType) -> TypeVarLikeType: diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test index 6568ba0b5f825..7c5c109ffea22 100644 --- a/test-data/unit/check-tuples.test +++ b/test-data/unit/check-tuples.test @@ -1831,10 +1831,3 @@ class tuple_aa_subclass(Tuple[A, A]): ... inst_tuple_aa_subclass: tuple_aa_subclass = tuple_aa_subclass((A(), A()))[:] # E: Incompatible types in assignment (expression has type "tuple[A, A]", variable has type "tuple_aa_subclass") [builtins fixtures/tuple.pyi] - -[case testTuplePassedParameters] -from typing_extensions import Concatenate - -def c(t: tuple[Concatenate[int, ...]]) -> None: # E: Invalid location for Concatenate # N: You can use Concatenate as the first argument to Callable - reveal_type(t) # N: Revealed type is "tuple[Any]" -[builtins fixtures/tuple.pyi] From 8198287a8582124eabcb58bbdcabea6b671ff422 Mon Sep 17 00:00:00 2001 From: cyphercodes Date: Thu, 7 May 2026 23:26:57 +0300 Subject: [PATCH 5/5] fix: narrow ParamSpec diagnostic type --- mypy/typeanal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 12d7325313b8c..20210bb438754 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1937,6 +1937,7 @@ def anal_type( self.fail("Invalid location for Concatenate", t, code=codes.VALID_TYPE) self.note("You can use Concatenate as the first argument to Callable", t) else: + assert isinstance(analyzed, ParamSpecType) self.fail( INVALID_PARAM_SPEC_LOCATION.format(format_type(analyzed, self.options)), t,