From 3eeb47b9aa717d84deada4ccdbcfdec03325810f Mon Sep 17 00:00:00 2001 From: Deepak kudi Date: Thu, 21 May 2026 17:15:05 +0530 Subject: [PATCH 1/2] Reject plain Concatenate ellipsis annotations --- mypy/typeanal.py | 25 ++++++++++++++++--- .../unit/check-parameter-specification.test | 3 +++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 0c0399d91ecca..e9187f8aaac07 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -248,6 +248,8 @@ def __init__( self.allow_final = allow_final # Are we in a context where ParamSpec literals are allowed? self.allow_param_spec_literals = allow_param_spec_literals + # Are we in a context where ParamSpec values are accepted directly? + self.allow_param_spec = False # Are we in context where literal "..." specifically is allowed? self.allow_ellipsis = False # Should we report an error whenever we encounter a RawExpressionType outside @@ -461,6 +463,22 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) special = self.try_analyze_special_unbound_type(t, fullname) if special is not None: return special + if node.fullname in CONCATENATE_TYPE_NAMES: + # Concatenate is an operator, no need for a proper type. Some callers analyze + # annotations directly via accept(), so validate direct invalid uses here. + result = self.apply_concatenate_operator(t) + if not (self.allow_param_spec or self.allow_param_spec_literals) and ( + isinstance(result, Parameters) + or ( + isinstance(result, ParamSpecType) + and result.flavor == ParamSpecFlavor.BARE + and result.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) + return AnyType(TypeOfAny.from_error) + return result if isinstance(node, TypeAlias): self.aliases_used.add(fullname) an_args = self.anal_array( @@ -508,10 +526,6 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) return self.analyze_type_with_type_info(node, t.args, t, t.empty_tuple_index) elif node.fullname in TYPE_ALIAS_NAMES: return AnyType(TypeOfAny.special_form) - # Concatenate is an operator, no need for a proper type - elif node.fullname in CONCATENATE_TYPE_NAMES: - # We check the return type further up the stack for valid use locations - return self.apply_concatenate_operator(t) else: return self.analyze_unbound_type_without_type_info(t, sym, defining_literal) else: # sym is None @@ -1918,6 +1932,8 @@ def anal_type( self.allow_final = allow_final old_allow_ellipsis = self.allow_ellipsis self.allow_ellipsis = allow_ellipsis + old_allow_param_spec = self.allow_param_spec + self.allow_param_spec = allow_param_spec old_allow_unpack = self.allow_unpack self.allow_unpack = allow_unpack try: @@ -1927,6 +1943,7 @@ def anal_type( self.nesting_level -= 1 self.allow_typed_dict_special_forms = old_allow_typed_dict_special_forms self.allow_ellipsis = old_allow_ellipsis + self.allow_param_spec = old_allow_param_spec self.allow_unpack = old_allow_unpack if ( not allow_param_spec diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 970ba45d0e8e2..f1572ab522766 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -28,6 +28,9 @@ def foo2(x: P) -> P: ... # E: Invalid location for ParamSpec "P" \ def foo3(x: Concatenate[int, P]) -> int: ... # E: Invalid location for Concatenate \ # N: You can use Concatenate as the first argument to Callable +plain_concatenate: Concatenate[...] # E: Invalid location for Concatenate \ + # N: You can use Concatenate as the first argument to Callable + def foo4(x: List[P]) -> None: ... # E: Invalid location for ParamSpec "P" \ # N: You can use ParamSpec as the first argument to Callable, e.g., "Callable[P, int]" From 6a8afdcb35dea2977384902642bca1e31af22803 Mon Sep 17 00:00:00 2001 From: Deepak kudi Date: Fri, 22 May 2026 12:26:23 +0530 Subject: [PATCH 2/2] Update Concatenate tuple test expectation --- test-data/unit/check-tuples.test | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test index bfbd2e631f5d8..e32a4e681d301 100644 --- a/test-data/unit/check-tuples.test +++ b/test-data/unit/check-tuples.test @@ -1835,6 +1835,7 @@ 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]