From acfc5bc6a25ed489fd113d9255d5b131db29fbc8 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Wed, 19 Aug 2026 18:52:58 -0700 Subject: [PATCH 1/7] PEP 718 prototype written by Fable --- mypy/checkexpr.py | 154 ++++++++++++++++++++++++++++++- mypy/options.py | 5 +- mypy/semanal.py | 49 +++++++++- test-data/unit/check-pep718.test | 111 ++++++++++++++++++++++ 4 files changed, 316 insertions(+), 3 deletions(-) create mode 100644 test-data/unit/check-pep718.test diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 172d44555b946..106b49687a816 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -111,7 +111,7 @@ YieldFromExpr, get_member_expr_fullname, ) -from mypy.options import PRECISE_TUPLE_TYPES +from mypy.options import PRECISE_TUPLE_TYPES, SUBSCRIPTABLE_FUNCTIONS from mypy.plugin import ( FunctionContext, FunctionSigContext, @@ -211,6 +211,7 @@ ) from mypy.typestate import type_state from mypy.typevars import fill_typevars +from mypy.util import plural_s from mypy.visitor import ExpressionVisitor # Type of callback user for checking individual function arguments. See @@ -4622,6 +4623,19 @@ def visit_index_with_type( ): return self.named_type("types.GenericAlias") + if ( + isinstance(left_type, FunctionLike) + and not left_type.is_type_obj() + and SUBSCRIPTABLE_FUNCTIONS in self.chk.options.enable_incomplete_feature + ): + # PEP 718: subscription of a generic function-like value that was + # not recognized during semantic analysis (e.g. a bound method or + # a variable of Callable type). The index was analyzed as a value + # expression, so re-parse it as one or more type expressions. + type_args = self.parse_index_as_type_arguments(index) + if type_args is not None: + return self.apply_subscript_to_function(left_type, type_args, e) + if isinstance(left_type, TypeVarType): return self.visit_index_with_type( left_type.values_or_bound(), e, original_type, left_type @@ -4992,6 +5006,9 @@ def visit_type_application(self, tapp: TypeApplication) -> Type: tp = get_proper_type(self.accept(tapp.expr)) if isinstance(tp, (CallableType, Overloaded)): if not tp.is_type_obj(): + if SUBSCRIPTABLE_FUNCTIONS in self.chk.options.enable_incomplete_feature: + # PEP 718: subscription of a generic function. + return self.apply_subscript_to_function(tp, tapp.types, tapp) self.chk.fail(message_registry.ONLY_CLASS_APPLICATION, tapp) return self.apply_type_arguments_to_callable(tp, tapp.types, tapp) if isinstance(tp, AnyType): @@ -5131,6 +5148,141 @@ class C(Generic[T, Unpack[Ts]]): ... start, middle, end = split_with_prefix_and_suffix(tuple(args), prefix, suffix) return list(start) + [TupleType(list(middle), tvt.tuple_fallback)] + list(end) + def parse_index_as_type_arguments(self, index: Expression) -> list[Type] | None: + """Parse the index of a subscript expression as type arguments (PEP 718). + + Returns None if any item cannot be interpreted as a type expression, + in which case the caller should fall back to regular __getitem__ + semantics. + """ + items = index.items if isinstance(index, TupleExpr) else [index] + type_args: list[Type] = [] + for item in items: + typ = self.try_parse_as_type_expression(item) + if typ is None: + return None + type_args.append(typ) + return type_args + + def apply_subscript_to_function(self, tp: Type, args: Sequence[Type], ctx: Context) -> Type: + """Apply type arguments to a generic function (PEP 718). + + Unlike apply_type_arguments_to_callable, this: + - requires the callable to actually be generic (subscripting a + non-generic function is a type error, even though the runtime + permits it); + - for overloads, implements the PEP 718 pre-filtering step: only + overload items where subscription may succeed (i.e. the number of + type arguments fits the item's type parameters) are retained. + """ + tp = get_proper_type(tp) + if isinstance(tp, CallableType): + result = self.apply_subscript_to_callable_item(tp, args, ctx, report=True) + if result is None: + return AnyType(TypeOfAny.from_error) + return result + assert isinstance(tp, Overloaded) + matching: list[CallableType] = [] + for it in tp.items: + applied = self.apply_subscript_to_callable_item(it, args, ctx, report=False) + if applied is not None: + matching.append(applied) + if not matching: + self.chk.fail( + f"No overload variant accepts {len(args)} type argument{plural_s(len(args))}", + ctx, + ) + return AnyType(TypeOfAny.from_error) + if len(matching) == 1: + return matching[0] + return Overloaded(matching) + + def subscriptable_own_type_vars( + self, tp: CallableType + ) -> tuple[int, list[TypeVarLikeType]]: + """Return (number of skipped leading class type vars, function's own type vars). + + Per PEP 718 binding rules, subscription of a method (including + classmethods accessed on an unspecialized generic class) binds only + the function's own type parameters, never the enclosing class's. + Class-level type variables always appear before the function's own + in tp.variables. + """ + tvars = list(tp.variables) + defn = tp.definition + if defn is not None and defn.fullname: + # A type variable belongs to the enclosing class if its namespace + # is a proper ancestor of the definition's fullname (e.g. T with + # namespace "mod.C" for a method "mod.C.cm"). The function's own + # type variables have the definition's fullname as namespace, or + # an empty namespace for methods accessed on an unspecialized + # class object. Class type variables always precede the + # function's own, so we take the longest valid suffix. + fullname = defn.fullname + own_suffix = 0 + for v in reversed(tvars): + ns = v.id.namespace + if ns and ns != fullname and fullname.startswith(ns + "."): + break + own_suffix += 1 + if 0 < own_suffix < len(tvars): + return len(tvars) - own_suffix, tvars[-own_suffix:] + return 0, tvars + + def apply_subscript_to_callable_item( + self, it: CallableType, args: Sequence[Type], ctx: Context, *, report: bool + ) -> CallableType | None: + """Apply PEP 718 subscription args to a single callable, or return None. + + None means this item does not accept this subscription; an error is + reported only if report is True. + """ + n_skip, type_vars = self.subscriptable_own_type_vars(it) + if not type_vars: + if report: + self.chk.fail("Cannot subscript a function with no type parameters", ctx) + return None + min_arg_count = sum(not v.has_default() for v in type_vars) + has_type_var_tuple = any(isinstance(v, TypeVarTupleType) for v in type_vars) + if not (has_type_var_tuple or min_arg_count <= len(args) <= len(type_vars)): + if report: + self.msg.incompatible_type_application( + min_arg_count, len(type_vars), len(args), ctx + ) + return None + padded: list[Type] = flatten_nested_tuples(args) + if has_type_var_tuple: + # Pack arguments around the (single) TypeVarTuple into a tuple, + # like split_for_callable does for class objects (that code path + # only supports type objects, so we do it here for functions). + tvt_index = next( + i for i, v in enumerate(type_vars) if isinstance(v, TypeVarTupleType) + ) + n_suffix_fixed = len(type_vars) - tvt_index - 1 + if len(padded) < tvt_index + n_suffix_fixed: + if report: + self.msg.incompatible_type_application( + tvt_index + n_suffix_fixed, len(type_vars), len(padded), ctx + ) + return None + end = len(padded) - n_suffix_fixed + middle = TupleType(padded[tvt_index:end], self.chk.named_type("builtins.tuple")) + padded = padded[:tvt_index] + [middle] + padded[end:] + elif len(padded) < len(type_vars): + # PEP 696: fill in omitted trailing type arguments from their + # defaults, so that e.g. for `def g[T, U = int](...)`, `g[str]` + # binds U to int (defaults may reference earlier type vars). + env: dict[TypeVarId, Type] = { + v.id: a for v, a in zip(type_vars, padded) + } + for v in type_vars[len(padded) :]: + d = expand_type(v.default, env) + padded.append(d) + env[v.id] = d + full_args: list[Type | None] = [None] * n_skip + full_args.extend(padded) + return self.apply_generic_arguments(it, full_args, ctx) + def apply_type_arguments_to_callable( self, tp: Type, args: Sequence[Type], ctx: Context ) -> Type: diff --git a/mypy/options.py b/mypy/options.py index e38ce8ba9e5d2..d79fd21a31f10 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -95,7 +95,10 @@ class BuildType: NEW_GENERIC_SYNTAX: Final = "NewGenericSyntax" INLINE_TYPEDDICT: Final = "InlineTypedDict" TYPE_FORM: Final = "TypeForm" -INCOMPLETE_FEATURES: Final = frozenset((PRECISE_TUPLE_TYPES, INLINE_TYPEDDICT)) +SUBSCRIPTABLE_FUNCTIONS: Final = "SubscriptableFunctions" +INCOMPLETE_FEATURES: Final = frozenset( + (PRECISE_TUPLE_TYPES, INLINE_TYPEDDICT, SUBSCRIPTABLE_FUNCTIONS) +) COMPLETE_FEATURES: Final = frozenset((TYPE_VAR_TUPLE, UNPACK, NEW_GENERIC_SYNTAX, TYPE_FORM)) diff --git a/mypy/semanal.py b/mypy/semanal.py index 7f961687a8aee..0ea95f3a41a46 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -197,7 +197,7 @@ type_aliases_source_versions, typing_extensions_aliases, ) -from mypy.options import Options +from mypy.options import SUBSCRIPTABLE_FUNCTIONS, Options from mypy.patterns import ( AsPattern, ClassPattern, @@ -6354,6 +6354,53 @@ def analyze_type_application_args(self, expr: IndexExpr) -> list[Type] | None: ) has_param_spec = base.node.has_param_spec_type num_args = len(base.node.type_vars) + elif ( + isinstance(base, RefExpr) + and isinstance(base.node, (FuncDef, OverloadedFuncDef)) + and SUBSCRIPTABLE_FUNCTIONS in self.options.enable_incomplete_feature + ): + # PEP 718: subscription of a generic function. Allow ParamSpec + # literals and Unpack based on the function's type parameters, + # mirroring the rules for generic classes. For PEP 695 syntax, + # read the unanalyzed type_args, since the analyzed signature's + # variables may not be bound yet at this point; fall back to the + # analyzed variables for old-style type variables. + allow_unpack = False + has_param_spec = False + num_args = 0 + defs: list[FuncDef] = [] + if isinstance(base.node, FuncDef): + defs = [base.node] + else: + for ovl_item in base.node.items: + fi = ovl_item.func if isinstance(ovl_item, Decorator) else ovl_item + if isinstance(fi, FuncDef): + defs.append(fi) + for fd in defs: + if fd.type_args is not None: + allow_unpack |= any( + tparam.kind == TYPE_VAR_TUPLE_KIND for tparam in fd.type_args + ) + has_param_spec |= any( + tparam.kind == PARAM_SPEC_KIND for tparam in fd.type_args + ) + num_args += len(fd.type_args) + elif isinstance(fd.type, CallableType): + if not fd.type.variables: + # Old-style type variables may not be bound into the + # signature yet at this point; be permissive here per + # the TODO above and let checkexpr validate. + allow_unpack = True + has_param_spec = True + num_args = -1 + else: + allow_unpack |= any( + isinstance(v, TypeVarTupleType) for v in fd.type.variables + ) + has_param_spec |= any( + isinstance(v, ParamSpecType) for v in fd.type.variables + ) + num_args += len(fd.type.variables) else: allow_unpack = False has_param_spec = False diff --git a/test-data/unit/check-pep718.test b/test-data/unit/check-pep718.test new file mode 100644 index 0000000000000..cc52ddedd46b9 --- /dev/null +++ b/test-data/unit/check-pep718.test @@ -0,0 +1,111 @@ +-- Tests for PEP 718 subscriptable functions (--enable-incomplete-feature=SubscriptableFunctions) + +[case testPEP718Basic] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +def make_list[T](*args: T) -> list[T]: + return list(args) # E: No overload variant of "list" matches argument type "tuple[T, ...]" \ + # N: Possible overload variants: \ + # N: def [T] list() -> list[T] \ + # N: def [T] list(x: Iterable[T]) -> list[T] +reveal_type(make_list[int]) # N: Revealed type is "def (*args: builtins.int) -> builtins.list[builtins.int]" +reveal_type(make_list[int]()) # N: Revealed type is "builtins.list[builtins.int]" +make_int_list = make_list[int] +reveal_type(make_int_list()) # N: Revealed type is "builtins.list[builtins.int]" +make_list[int]("a") # E: Argument 1 to "make_list" has incompatible type "str"; expected "int" +[builtins fixtures/list.pyi] + +[case testPEP718Unsolvable] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +def foo[T](x: list[T]) -> T: ... +reveal_type(foo[int]([])) # N: Revealed type is "builtins.int" +words = ["a", "b"] +foo[int](words) # E: Argument 1 to "foo" has incompatible type "list[str]"; expected "list[int]" +[builtins fixtures/list.pyi] + +[case testPEP718Errors] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +def plain(x: int) -> int: + return x +def foo[T](x: T) -> T: ... +plain[int] # E: Cannot subscript a function with no type parameters +foo[int, str] # E: Type application has too many types (1 expected) +[builtins fixtures/tuple.pyi] + +[case testPEP718Disabled] +# flags: --python-version=3.12 +def foo[T](x: T) -> T: ... +foo[int] # E: Type application is only supported for generic classes +[builtins fixtures/tuple.pyi] + +[case testPEP718Defaults] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +from typing import TypeVar +T = TypeVar("T") +U = TypeVar("U", default=int) +def g(x: T, y: U) -> tuple[T, U]: ... +reveal_type(g[str]) # N: Revealed type is "def (x: builtins.str, y: builtins.int) -> tuple[builtins.str, builtins.int]" +g[str]("", "") # E: Argument 2 to "g" has incompatible type "str"; expected "int" +reveal_type(g[str, bytes]) # N: Revealed type is "def (x: builtins.str, y: builtins.bytes) -> tuple[builtins.str, builtins.bytes]" +[builtins fixtures/tuple.pyi] + +[case testPEP718DefaultReferencesEarlierTypeVar] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +from typing import TypeVar +S = TypeVar("S") +T = TypeVar("T", default=S) +def g(x: S, y: T) -> tuple[S, T]: ... +reveal_type(g[str]) # N: Revealed type is "def (x: builtins.str, y: builtins.str) -> tuple[builtins.str, builtins.str]" +[builtins fixtures/tuple.pyi] + +[case testPEP718Methods] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +class C[T]: + def method[U](self, x: T, y: U) -> U: ... + @classmethod + def cm[U](cls, y: U) -> U: ... +c: C[int] +reveal_type(c.method[str]) # N: Revealed type is "def (x: builtins.int, y: builtins.str) -> builtins.str" +c.method[str](0, 0) # E: Argument 2 to "method" of "C" has incompatible type "int"; expected "str" +reveal_type(C.cm[str]("")) # N: Revealed type is "builtins.str" +[builtins fixtures/classmethod.pyi] + +[case testPEP718CallableTypedValue] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +from typing import Callable +def keep[F: Callable](f: F) -> F: + return f +@keep +def dec_fn[T](x: T) -> list[T]: ... +reveal_type(dec_fn[int]) # N: Revealed type is "def (x: builtins.int) -> builtins.list[builtins.int]" +def erase(f: Callable[..., object]) -> Callable[..., object]: + return f +@erase +def erased[T](x: T) -> T: ... +erased[int] # E: Cannot subscript a function with no type parameters +[builtins fixtures/list.pyi] + +[case testPEP718OverloadFiltering] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +from typing import overload +@overload +def pick[T](x: T) -> list[T]: ... +@overload +def pick[S, U](x: S, y: U) -> dict[S, U]: ... +def pick(x, y=None): ... +reveal_type(pick[int]) # N: Revealed type is "def (x: builtins.int) -> builtins.list[builtins.int]" +reveal_type(pick[str, int]("a", 1)) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" +pick[int, str, bytes] # E: No overload variant accepts 3 type arguments +[builtins fixtures/dict.pyi] + +[case testPEP718ParamSpec] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +from typing import Callable +def wrap[**P, R](f: Callable[P, R]) -> Callable[P, R]: ... +reveal_type(wrap[[int], str]) # N: Revealed type is "def (f: def (builtins.int) -> builtins.str) -> def (builtins.int) -> builtins.str" +[builtins fixtures/tuple.pyi] + +[case testPEP718TypeVarTuple] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +def variadic[*Ts](*args: *tuple[*Ts]) -> tuple[*Ts]: ... +reveal_type(variadic[int, str]) # N: Revealed type is "def (builtins.int, builtins.str) -> tuple[builtins.int, builtins.str]" +[builtins fixtures/tuple.pyi] From 730b60da1c1b0737637653197a5fd4b5f12ef17c Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 20 Aug 2026 16:15:50 -0700 Subject: [PATCH 2/7] Fix for phantom tvars by Fable --- mypy/checkexpr.py | 18 +++++++++++++++++- mypy/semanal.py | 23 +++++++++++++++++++++++ test-data/unit/check-pep718.test | 29 +++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 106b49687a816..f7e9c699dc652 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5281,7 +5281,23 @@ def apply_subscript_to_callable_item( env[v.id] = d full_args: list[Type | None] = [None] * n_skip full_args.extend(padded) - return self.apply_generic_arguments(it, full_args, ctx) + if report: + return self.apply_generic_arguments(it, full_args, ctx) + # Silent mode (PEP 718 overload pre-filtering): a bound or constraint + # violation means this item does not accept the subscription — e.g. + # `ser[int]` must skip an overload whose type parameter is bounded by + # str. Capture violations instead of reporting them. + violations: list[str] = [] + + def note_violation( + _callable: CallableType, _typ: Type, name: str, _context: Context + ) -> None: + violations.append(name) + + applied = applytype.apply_generic_arguments(it, full_args, note_violation, ctx) + if violations: + return None + return applied def apply_type_arguments_to_callable( self, tp: Type, args: Sequence[Type], ctx: Context diff --git a/mypy/semanal.py b/mypy/semanal.py index 0ea95f3a41a46..f88b6f20ead7e 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1242,6 +1242,29 @@ def update_function_type_variables(self, fun_type: CallableType, defn: FuncItem) self.msg.type_parameters_should_be_declared( [n.split(".")[-1] for n in extra], defn ) + if SUBSCRIPTABLE_FUNCTIONS in self.options.enable_incomplete_feature: + # PEP 718 prototype: a function is generic over *all* its + # declared type parameters, in declaration order — even + # ones that don't appear in the signature (which explicit + # subscription can now observe). Without this, variables + # holds only the inferred (signature-appearance ordered) + # subset. + by_name = {v.name: v for v in fun_type.variables} + new_vars: list[TypeVarLikeType] = [] + for p in defn.type_args: + if p.name in by_name: + new_vars.append(by_name.pop(p.name)) + continue + sym = self.lookup_qualified(p.name, defn) + if sym is None or not isinstance(sym.node, TypeVarLikeExpr): + continue + new_vars.append( + a.tvar_scope.bind_new(p.name, sym.node, a.fail_func, fun_type) + ) + # Defensively keep anything bound but not declared (already + # reported above) at the end. + new_vars.extend(by_name.values()) + fun_type.variables = tuple(new_vars) return has_self_type def setup_self_type(self) -> None: diff --git a/test-data/unit/check-pep718.test b/test-data/unit/check-pep718.test index cc52ddedd46b9..8130735a2efcf 100644 --- a/test-data/unit/check-pep718.test +++ b/test-data/unit/check-pep718.test @@ -109,3 +109,32 @@ reveal_type(wrap[[int], str]) # N: Revealed type is "def (f: def (builtins.int) def variadic[*Ts](*args: *tuple[*Ts]) -> tuple[*Ts]: ... reveal_type(variadic[int, str]) # N: Revealed type is "def (builtins.int, builtins.str) -> tuple[builtins.int, builtins.str]" [builtins fixtures/tuple.pyi] + +[case testPEP718PhantomTypeParams] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +def f[T]() -> None: pass +reveal_type(f) # N: Revealed type is "def [T] ()" +reveal_type(f[int]) # N: Revealed type is "def ()" +f[int, str] # E: Type application has too many types (1 expected) +[builtins fixtures/tuple.pyi] + +[case testPEP718DeclarationOrder] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +def g[T, U](x: U, y: T) -> tuple[T, U]: ... +reveal_type(g[int, str]) # N: Revealed type is "def (x: builtins.str, y: builtins.int) -> tuple[builtins.int, builtins.str]" +[builtins fixtures/tuple.pyi] + +[case testPEP718OverloadDispatchOnBounds] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +from typing import overload +class StringSerializer: ... +class IntSerializer: ... +@overload +def ser[T: str]() -> StringSerializer: ... +@overload +def ser[T: int]() -> IntSerializer: ... # E: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader +def ser(): ... +reveal_type(ser[str]()) # N: Revealed type is "__main__.StringSerializer" +reveal_type(ser[int]()) # N: Revealed type is "__main__.IntSerializer" +ser[bytes]() # E: No overload variant accepts 1 type argument +[builtins fixtures/tuple.pyi] From 937ae1deff9284249603ef8c24e935f63153c351 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:15:17 +0000 Subject: [PATCH 3/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkexpr.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f7e9c699dc652..d54d90aad0195 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5189,17 +5189,14 @@ def apply_subscript_to_function(self, tp: Type, args: Sequence[Type], ctx: Conte matching.append(applied) if not matching: self.chk.fail( - f"No overload variant accepts {len(args)} type argument{plural_s(len(args))}", - ctx, + f"No overload variant accepts {len(args)} type argument{plural_s(len(args))}", ctx ) return AnyType(TypeOfAny.from_error) if len(matching) == 1: return matching[0] return Overloaded(matching) - def subscriptable_own_type_vars( - self, tp: CallableType - ) -> tuple[int, list[TypeVarLikeType]]: + def subscriptable_own_type_vars(self, tp: CallableType) -> tuple[int, list[TypeVarLikeType]]: """Return (number of skipped leading class type vars, function's own type vars). Per PEP 718 binding rules, subscription of a method (including @@ -5255,9 +5252,7 @@ def apply_subscript_to_callable_item( # Pack arguments around the (single) TypeVarTuple into a tuple, # like split_for_callable does for class objects (that code path # only supports type objects, so we do it here for functions). - tvt_index = next( - i for i, v in enumerate(type_vars) if isinstance(v, TypeVarTupleType) - ) + tvt_index = next(i for i, v in enumerate(type_vars) if isinstance(v, TypeVarTupleType)) n_suffix_fixed = len(type_vars) - tvt_index - 1 if len(padded) < tvt_index + n_suffix_fixed: if report: @@ -5272,9 +5267,7 @@ def apply_subscript_to_callable_item( # PEP 696: fill in omitted trailing type arguments from their # defaults, so that e.g. for `def g[T, U = int](...)`, `g[str]` # binds U to int (defaults may reference earlier type vars). - env: dict[TypeVarId, Type] = { - v.id: a for v, a in zip(type_vars, padded) - } + env: dict[TypeVarId, Type] = {v.id: a for v, a in zip(type_vars, padded)} for v in type_vars[len(padded) :]: d = expand_type(v.default, env) padded.append(d) From 4cee9adace9d71f8b3fa65980a4bd94f73ee0e2b Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Sat, 24 Oct 2026 11:58:43 -0700 Subject: [PATCH 4/7] Reject PEP 718 before 3.16 (with tests) --- mypy/checkexpr.py | 3 + test-data/unit/check-pep718.test | 113 +++++++++++++++++++++++++++---- 2 files changed, 102 insertions(+), 14 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index d54d90aad0195..7cc5c1f023784 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5175,6 +5175,9 @@ def apply_subscript_to_function(self, tp: Type, args: Sequence[Type], ctx: Conte overload items where subscription may succeed (i.e. the number of type arguments fits the item's type parameters) are retained. """ + if self.chk.options.python_version < (3, 16): + self.chk.fail("Function subscription requires Python 3.16 or newer", ctx) + return AnyType(TypeOfAny.from_error) tp = get_proper_type(tp) if isinstance(tp, CallableType): result = self.apply_subscript_to_callable_item(tp, args, ctx, report=True) diff --git a/test-data/unit/check-pep718.test b/test-data/unit/check-pep718.test index 8130735a2efcf..e353cac320101 100644 --- a/test-data/unit/check-pep718.test +++ b/test-data/unit/check-pep718.test @@ -1,7 +1,7 @@ -- Tests for PEP 718 subscriptable functions (--enable-incomplete-feature=SubscriptableFunctions) [case testPEP718Basic] -# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 def make_list[T](*args: T) -> list[T]: return list(args) # E: No overload variant of "list" matches argument type "tuple[T, ...]" \ # N: Possible overload variants: \ @@ -15,7 +15,7 @@ make_list[int]("a") # E: Argument 1 to "make_list" has incompatible type "str"; [builtins fixtures/list.pyi] [case testPEP718Unsolvable] -# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 def foo[T](x: list[T]) -> T: ... reveal_type(foo[int]([])) # N: Revealed type is "builtins.int" words = ["a", "b"] @@ -23,7 +23,7 @@ foo[int](words) # E: Argument 1 to "foo" has incompatible type "list[str]"; exp [builtins fixtures/list.pyi] [case testPEP718Errors] -# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 def plain(x: int) -> int: return x def foo[T](x: T) -> T: ... @@ -32,13 +32,13 @@ foo[int, str] # E: Type application has too many types (1 expected) [builtins fixtures/tuple.pyi] [case testPEP718Disabled] -# flags: --python-version=3.12 +# flags: --python-version=3.16 def foo[T](x: T) -> T: ... foo[int] # E: Type application is only supported for generic classes [builtins fixtures/tuple.pyi] [case testPEP718Defaults] -# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 from typing import TypeVar T = TypeVar("T") U = TypeVar("U", default=int) @@ -49,7 +49,7 @@ reveal_type(g[str, bytes]) # N: Revealed type is "def (x: builtins.str, y: buil [builtins fixtures/tuple.pyi] [case testPEP718DefaultReferencesEarlierTypeVar] -# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 from typing import TypeVar S = TypeVar("S") T = TypeVar("T", default=S) @@ -58,7 +58,7 @@ reveal_type(g[str]) # N: Revealed type is "def (x: builtins.str, y: builtins.st [builtins fixtures/tuple.pyi] [case testPEP718Methods] -# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 class C[T]: def method[U](self, x: T, y: U) -> U: ... @classmethod @@ -70,7 +70,7 @@ reveal_type(C.cm[str]("")) # N: Revealed type is "builtins.str" [builtins fixtures/classmethod.pyi] [case testPEP718CallableTypedValue] -# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 from typing import Callable def keep[F: Callable](f: F) -> F: return f @@ -85,7 +85,7 @@ erased[int] # E: Cannot subscript a function with no type parameters [builtins fixtures/list.pyi] [case testPEP718OverloadFiltering] -# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 from typing import overload @overload def pick[T](x: T) -> list[T]: ... @@ -98,20 +98,20 @@ pick[int, str, bytes] # E: No overload variant accepts 3 type arguments [builtins fixtures/dict.pyi] [case testPEP718ParamSpec] -# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 from typing import Callable def wrap[**P, R](f: Callable[P, R]) -> Callable[P, R]: ... reveal_type(wrap[[int], str]) # N: Revealed type is "def (f: def (builtins.int) -> builtins.str) -> def (builtins.int) -> builtins.str" [builtins fixtures/tuple.pyi] [case testPEP718TypeVarTuple] -# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 def variadic[*Ts](*args: *tuple[*Ts]) -> tuple[*Ts]: ... reveal_type(variadic[int, str]) # N: Revealed type is "def (builtins.int, builtins.str) -> tuple[builtins.int, builtins.str]" [builtins fixtures/tuple.pyi] [case testPEP718PhantomTypeParams] -# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 def f[T]() -> None: pass reveal_type(f) # N: Revealed type is "def [T] ()" reveal_type(f[int]) # N: Revealed type is "def ()" @@ -119,13 +119,13 @@ f[int, str] # E: Type application has too many types (1 expected) [builtins fixtures/tuple.pyi] [case testPEP718DeclarationOrder] -# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 def g[T, U](x: U, y: T) -> tuple[T, U]: ... reveal_type(g[int, str]) # N: Revealed type is "def (x: builtins.str, y: builtins.int) -> tuple[builtins.int, builtins.str]" [builtins fixtures/tuple.pyi] [case testPEP718OverloadDispatchOnBounds] -# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 from typing import overload class StringSerializer: ... class IntSerializer: ... @@ -138,3 +138,88 @@ reveal_type(ser[str]()) # N: Revealed type is "__main__.StringSerializer" reveal_type(ser[int]()) # N: Revealed type is "__main__.IntSerializer" ser[bytes]() # E: No overload variant accepts 1 type argument [builtins fixtures/tuple.pyi] + +[case testPEP718Python310Rejected] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.10 +from typing import Callable, TypeVar +T = TypeVar("T") +def identity(x: T) -> T: ... +identity[int] # E: Function subscription requires Python 3.16 or newer +def use_callable(f: Callable[[T], T]) -> None: + f[int] # E: Function subscription requires Python 3.16 or newer +[builtins fixtures/tuple.pyi] + +[case testPEP718Python311Rejected] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.11 +from typing import Callable, TypeVar +T = TypeVar("T") +def identity(x: T) -> T: ... +identity[int] # E: Function subscription requires Python 3.16 or newer +def use_callable(f: Callable[[T], T]) -> None: + f[int] # E: Function subscription requires Python 3.16 or newer +[builtins fixtures/tuple.pyi] + +[case testPEP718Python312Rejected] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.12 +from typing import Callable, TypeVar +T = TypeVar("T") +def identity(x: T) -> T: ... +identity[int] # E: Function subscription requires Python 3.16 or newer +def use_callable(f: Callable[[T], T]) -> None: + f[int] # E: Function subscription requires Python 3.16 or newer +[builtins fixtures/tuple.pyi] + +[case testPEP718Python313Rejected] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.13 +from typing import Callable, TypeVar +T = TypeVar("T") +def identity(x: T) -> T: ... +identity[int] # E: Function subscription requires Python 3.16 or newer +def use_callable(f: Callable[[T], T]) -> None: + f[int] # E: Function subscription requires Python 3.16 or newer +[builtins fixtures/tuple.pyi] + +[case testPEP718Python314Rejected] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.14 +from typing import Callable, TypeVar +T = TypeVar("T") +def identity(x: T) -> T: ... +identity[int] # E: Function subscription requires Python 3.16 or newer +def use_callable(f: Callable[[T], T]) -> None: + f[int] # E: Function subscription requires Python 3.16 or newer +[builtins fixtures/tuple.pyi] + +[case testPEP718Python315Rejected] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.15 +from typing import Callable, Generic, TypeVar, overload +T = TypeVar("T") +def identity(x: T) -> T: ... +identity[int] # E: Function subscription requires Python 3.16 or newer +alias = identity +alias[int] # E: Function subscription requires Python 3.16 or newer +class C: + def method(self, x: T) -> T: ... + @classmethod + def cm(cls, x: T) -> T: ... + @staticmethod + def sm(x: T) -> T: ... +C.method[int] # E: Function subscription requires Python 3.16 or newer +C().method[int] # E: Function subscription requires Python 3.16 or newer +C.cm[int] # E: Function subscription requires Python 3.16 or newer +C.sm[int] # E: Function subscription requires Python 3.16 or newer +@overload +def pick(x: T) -> T: ... +@overload +def pick(x: T, y: T) -> T: ... +def pick(x, y=None): ... +pick[int] # E: Function subscription requires Python 3.16 or newer +def use_callable(f: Callable[[T], T]) -> None: + f[int] # E: Function subscription requires Python 3.16 or newer +# Class subscription and ordinary __getitem__ must remain valid. +class Box(Generic[T]): pass +reveal_type(Box[int]()) # N: Revealed type is "__main__.Box[builtins.int]" +class Indexable: + def __call__(self, x: T) -> T: ... + def __getitem__(self, key: int) -> str: ... +reveal_type(Indexable()[0]) # N: Revealed type is "builtins.str" +[builtins fixtures/classmethod.pyi] From e3bfbee2e9f9a5d8e0c6f25688e05b4e348501c6 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Sat, 24 Oct 2026 12:41:47 -0700 Subject: [PATCH 5/7] Implement the remaining PEP 718 typing rules - return-only type parameter inference - overload checks - method binding - ParamSpec handling - reject multiple TypeVarTuples without crashing - preserve generic parameters through decorators - support function alias attributes and resubscription Includes a bundled typeshed update and regression tests. --- mypy/checker.py | 86 ++++++++++- mypy/checkexpr.py | 189 ++++++++++++++++++++---- mypy/typeshed/stdlib/types.pyi | 13 ++ test-data/unit/check-pep718.test | 246 ++++++++++++++++++++++++++++++- 4 files changed, 497 insertions(+), 37 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 33ed5387554d8..eac1186bee579 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -210,7 +210,7 @@ def __init__(self) -> None: is_final_node, ) from mypy.operators import flip_ops, int_op_to_method, neg_ops -from mypy.options import PRECISE_TUPLE_TYPES, Options +from mypy.options import PRECISE_TUPLE_TYPES, SUBSCRIPTABLE_FUNCTIONS, Options from mypy.patterns import AsPattern, StarredPattern from mypy.plugin import Plugin from mypy.plugins import dataclasses as dataclasses_plugin @@ -246,6 +246,7 @@ def __init__(self) -> None: false_only, fixup_partial_type, function_type, + get_type_vars, is_literal_type_like, is_singleton_equality_type, is_singleton_identity_type, @@ -273,6 +274,7 @@ def __init__(self) -> None: LiteralType, NoneType, Overloaded, + ParamSpecType, PartialType, ProperType, TupleType, @@ -1048,6 +1050,16 @@ def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None: # decorator or if the implementation is untyped -- we gave up on the types. impl_type = self.extract_callable_type(inner_type, defn.impl) + if defn.impl and SUBSCRIPTABLE_FUNCTIONS in self.options.enable_incomplete_feature: + implementation = defn.impl.func if isinstance(defn.impl, Decorator) else defn.impl + if not implementation.type_args and any( + isinstance(item, Decorator) and item.func.type_args for item in defn.items + ): + self.fail( + "Overload implementation must declare type parameters when its overloads do", + implementation, + ) + is_descriptor_get = defn.info and defn.name == "__get__" for i, item in enumerate(defn.items): assert isinstance(item, Decorator) @@ -1064,6 +1076,12 @@ def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None: if not are_argument_counts_overlapping(sig1, sig2): continue + if ( + SUBSCRIPTABLE_FUNCTIONS in self.options.enable_incomplete_feature + and overloads_differ_in_typevar_bounds(sig1, sig2) + ): + continue + if overload_can_never_match(sig1, sig2): self.msg.overloaded_signature_will_never_match(i + 1, i + j + 2, item2.func) elif not is_descriptor_get: @@ -1824,6 +1842,8 @@ def is_var_redefined_in_outer_context(self, v: Var, after_line: int) -> bool: def check_unbound_return_typevar(self, typ: CallableType) -> None: """Fails when the return typevar is not defined in arguments.""" + if SUBSCRIPTABLE_FUNCTIONS in self.options.enable_incomplete_feature: + return if isinstance(typ.ret_type, TypeVarType) and typ.ret_type in typ.variables: arg_type_visitor = CollectArgTypeVarTypes() for argtype in typ.arg_types: @@ -5853,6 +5873,25 @@ def visit_decorator_inner( continue non_trivial_decorator = True dec = self.expr_checker.accept(d) + preserved: list[TypeVarLikeType] = [] + original_variables: list[TypeVarLikeType] = [] + sig = get_proper_type(sig) + dec_type = get_proper_type(dec) + if ( + SUBSCRIPTABLE_FUNCTIONS in self.options.enable_incomplete_feature + and isinstance(sig, CallableType) + and isinstance(dec_type, CallableType) + and any(isinstance(v, ParamSpecType) for v in dec_type.variables) + and isinstance(get_proper_type(dec_type.ret_type), CallableType) + ): + original_variables = list(sig.variables) + preserved = [ + v + for v in sig.variables + if v in get_type_vars(sig.ret_type) + and not any(v in get_type_vars(a) for a in sig.arg_types) + ] + sig = sig.copy_modified(variables=[v for v in sig.variables if v not in preserved]) temp = self.temp_node(sig, context=d) fullname = None if isinstance(d, RefExpr): @@ -5867,6 +5906,17 @@ def visit_decorator_inner( sig, t2 = self.expr_checker.check_call( dec, [temp], [nodes.ARG_POS], e, callable_name=fullname, object_type=object_type ) + sig = get_proper_type(sig) + if preserved and isinstance(sig, CallableType): + remaining = get_type_vars(sig) + variables = list(sig.variables) + [ + v for v in preserved if v in remaining and v not in sig.variables + ] + # Inference may freshen the other variables. Their fullnames + # still identify their position in the original declaration. + order = {v.fullname: i for i, v in enumerate(original_variables)} + variables.sort(key=lambda v: order.get(v.fullname, len(order))) + sig = sig.copy_modified(variables=variables) if non_trivial_decorator: self.check_untyped_after_decorator(sig, e.func) self.require_correct_self_argument(sig, e.func) @@ -9310,6 +9360,40 @@ def detach_callable(typ: CallableType, class_type_vars: list[TypeVarLikeType]) - return typ.copy_modified(variables=list(typ.variables) + class_type_vars) +def overloads_differ_in_typevar_bounds(first: CallableType, second: CallableType) -> bool: + """Can explicit type arguments distinguish otherwise identical parameter lists?""" + if not first.variables or len(first.variables) != len(second.variables): + return False + if first.arg_kinds != second.arg_kinds or first.arg_names != second.arg_names: + return False + if any(type(a) is not type(b) for a, b in zip(first.variables, second.variables)): + return False + substitutions = {b.id: a for a, b in zip(first.variables, second.variables)} + if any( + not is_same_type(a.default, expand_type(b.default, substitutions)) + for a, b in zip(first.variables, second.variables) + ): + return False + if not all( + is_same_type(a, expand_type(b, substitutions)) + for a, b in zip(first.arg_types, second.arg_types) + ): + return False + return any( + not is_same_type(a.upper_bound, expand_type(b.upper_bound, substitutions)) + or isinstance(a, TypeVarType) + and isinstance(b, TypeVarType) + and ( + len(a.values) != len(b.values) + or any( + not is_same_type(x, expand_type(y, substitutions)) + for x, y in zip(a.values, b.values) + ) + ) + for a, b in zip(first.variables, second.variables) + ) + + def overload_can_never_match(signature: CallableType, other: CallableType) -> bool: """Check if the 'other' method can never be matched due to 'signature'. diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 7cc5c1f023784..4e3f1dc697e28 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -65,6 +65,7 @@ Expression, FloatExpr, FuncDef, + FuncItem, GeneratorExpr, IndexExpr, IntExpr, @@ -2129,7 +2130,15 @@ def infer_function_type_arguments_using_context( # expects_literal(identity(3)) # Should type-check # TODO: we may want to add similar exception if all arguments are lambdas, since # in this case external context is almost everything we have. - if not is_generic_instance(ctx) and not is_literal_type_like(ctx): + return_only = ( + SUBSCRIPTABLE_FUNCTIONS in self.chk.options.enable_incomplete_feature + and any( + v in get_type_vars(ret_type) + and not any(v in get_type_vars(a) for a in callable.arg_types) + for v in callable.variables + ) + ) + if not return_only and not is_generic_instance(ctx) and not is_literal_type_like(ctx): return callable.copy_modified() args = infer_type_arguments( callable.variables, ret_type, erased_ctx, skip_unsatisfied=True @@ -2412,6 +2421,19 @@ def apply_inferred_arguments( # case assume that all variables have type Any to avoid extra # bogus error messages. for inferred_type, tv in zip(inferred_args, callee_type.variables): + if ( + SUBSCRIPTABLE_FUNCTIONS in self.chk.options.enable_incomplete_feature + and not tv.has_default() + and tv in get_type_vars(callee_type.ret_type) + and not any(tv in get_type_vars(a) for a in callee_type.arg_types) + and (inferred_type is None or has_ambiguous_uninhabited_component(inferred_type)) + ): + self.msg.could_not_infer_type_arguments(callee_type, tv, context) + inferred_args = list(inferred_args) + inferred_args[list(callee_type.variables).index(tv)] = AnyType( + TypeOfAny.from_error + ) + continue if not inferred_type or has_erased_component(inferred_type): # Could not infer a non-trivial type for a type variable. self.msg.could_not_infer_type_arguments(callee_type, tv, context) @@ -4571,6 +4593,19 @@ def visit_index_with_type( self_type = self_type or left_type left_type = get_proper_type(left_type) + if ( + isinstance(left_type, FunctionLike) + and not left_type.is_type_obj() + and SUBSCRIPTABLE_FUNCTIONS in self.chk.options.enable_incomplete_feature + ): + # PEP 718: subscription of a generic function-like value that was + # not recognized during semantic analysis (e.g. a bound method or + # a variable of Callable type). The index was analyzed as a value + # expression, so re-parse it as one or more type expressions. + type_args = self.parse_index_as_type_arguments(index) + if type_args is not None: + return self.apply_subscript_to_function(left_type, type_args, e) + # Visit the index, just to make sure we have a type for it available self.accept(index) @@ -4623,19 +4658,6 @@ def visit_index_with_type( ): return self.named_type("types.GenericAlias") - if ( - isinstance(left_type, FunctionLike) - and not left_type.is_type_obj() - and SUBSCRIPTABLE_FUNCTIONS in self.chk.options.enable_incomplete_feature - ): - # PEP 718: subscription of a generic function-like value that was - # not recognized during semantic analysis (e.g. a bound method or - # a variable of Callable type). The index was analyzed as a value - # expression, so re-parse it as one or more type expressions. - type_args = self.parse_index_as_type_arguments(index) - if type_args is not None: - return self.apply_subscript_to_function(left_type, type_args, e) - if isinstance(left_type, TypeVarType): return self.visit_index_with_type( left_type.values_or_bound(), e, original_type, left_type @@ -5158,7 +5180,23 @@ def parse_index_as_type_arguments(self, index: Expression) -> list[Type] | None: items = index.items if isinstance(index, TupleExpr) else [index] type_args: list[Type] = [] for item in items: - typ = self.try_parse_as_type_expression(item) + if isinstance(item, ListExpr): + parameters: list[Type] = [] + for parameter in item.items: + typ = self.try_parse_as_type_expression(parameter, allow_bound_type_vars=True) + if typ is None: + return None + parameters.append(typ) + typ = Parameters(parameters, [ARG_POS] * len(parameters), [None] * len(parameters)) + elif isinstance(item, EllipsisExpr): + typ = AnyType(TypeOfAny.special_form) + elif isinstance(item, StarExpr): + unpacked = self.try_parse_as_type_expression(item.expr, allow_bound_type_vars=True) + if unpacked is None: + return None + typ = UnpackType(unpacked) + else: + typ = self.try_parse_as_type_expression(item, allow_bound_type_vars=True) if typ is None: return None type_args.append(typ) @@ -5225,8 +5263,8 @@ def subscriptable_own_type_vars(self, tp: CallableType) -> tuple[int, list[TypeV if ns and ns != fullname and fullname.startswith(ns + "."): break own_suffix += 1 - if 0 < own_suffix < len(tvars): - return len(tvars) - own_suffix, tvars[-own_suffix:] + if own_suffix < len(tvars): + return len(tvars) - own_suffix, tvars[-own_suffix:] if own_suffix else [] return 0, tvars def apply_subscript_to_callable_item( @@ -5237,11 +5275,39 @@ def apply_subscript_to_callable_item( None means this item does not accept this subscription; an error is reported only if report is True. """ - n_skip, type_vars = self.subscriptable_own_type_vars(it) + alias_attrs = it.fallback.extra_attrs + alias_arguments: TupleType | None = None + alias_origin: Type = it + if alias_attrs and "__pep718_callable__" in alias_attrs.attrs: + template = get_proper_type(alias_attrs.attrs["__pep718_callable__"]) + assert isinstance(template, CallableType) + stored_arguments = get_proper_type(alias_attrs.attrs["__pep718_arguments__"]) + assert isinstance(stored_arguments, TupleType) + alias_arguments = stored_arguments + alias_origin = alias_attrs.attrs["__origin__"] + free: list[TypeVarLikeType] = [] + for argument in alias_arguments.items: + for variable in get_all_type_vars(argument): + if variable not in free: + free.append(variable) + enclosing = [v for v in template.variables if v not in free] + it = template.copy_modified(variables=enclosing + free, definition=None) + n_skip, type_vars = len(enclosing), free + else: + n_skip, type_vars = self.subscriptable_own_type_vars(it) if not type_vars: if report: self.chk.fail("Cannot subscript a function with no type parameters", ctx) return None + if sum(isinstance(v, TypeVarTupleType) for v in type_vars) > 1: + if report: + self.chk.fail("Cannot subscript a function with multiple TypeVarTuples", ctx) + return None + if len(type_vars) == 1 and isinstance(type_vars[0], ParamSpecType) and args: + if len(args) > 1 or not isinstance( + get_proper_type(args[0]), (Parameters, ParamSpecType, AnyType) + ): + args = [Parameters(list(args), [ARG_POS] * len(args), [None] * len(args))] min_arg_count = sum(not v.has_default() for v in type_vars) has_type_var_tuple = any(isinstance(v, TypeVarTupleType) for v in type_vars) if not (has_type_var_tuple or min_arg_count <= len(args) <= len(type_vars)): @@ -5275,10 +5341,24 @@ def apply_subscript_to_callable_item( d = expand_type(v.default, env) padded.append(d) env[v.id] = d + for variable, argument in zip(type_vars, padded): + if isinstance(variable, ParamSpecType) and not isinstance( + get_proper_type(argument), (Parameters, ParamSpecType, AnyType) + ): + if report: + self.chk.fail( + "Can only replace ParamSpec with a parameter types list or" + f" another ParamSpec, got {format_type(argument, self.chk.options)}", + ctx, + ) + return None full_args: list[Type | None] = [None] * n_skip full_args.extend(padded) if report: - return self.apply_generic_arguments(it, full_args, ctx) + applied = self.apply_generic_arguments(it, full_args, ctx) + return self.function_alias_type( + applied, alias_origin, args, alias_arguments, type_vars, padded + ) # Silent mode (PEP 718 overload pre-filtering): a bound or constraint # violation means this item does not accept the subscription — e.g. # `ser[int]` must skip an overload whose type parameter is bounded by @@ -5293,7 +5373,46 @@ def note_violation( applied = applytype.apply_generic_arguments(it, full_args, note_violation, ctx) if violations: return None - return applied + return self.function_alias_type( + applied, alias_origin, args, alias_arguments, type_vars, padded + ) + + def function_alias_type( + self, + signature: CallableType, + origin: Type, + args: Sequence[Type], + previous_args: TupleType | None, + variables: Sequence[TypeVarLikeType], + replacements: Sequence[Type], + ) -> CallableType: + """Keep a precise call signature together with runtime alias attributes. + + The fallback's extra attributes are serialized with the callable. The + private attributes retain free alias parameters for a later subscription; + they must not make an ordinary call infer enclosing-scope type variables. + """ + tuple_type = self.chk.named_generic_type( + "builtins.tuple", [AnyType(TypeOfAny.special_form)] + ) + if previous_args is not None: + env = {v.id: a for v, a in zip(variables, replacements)} + arguments = expand_type(previous_args, env) + else: + arguments = TupleType(list(args), tuple_type) + fallback = signature.fallback + types_module = self.chk.modules.get("types") + if types_module is not None and "FunctionGenericAlias" in types_module.names: + fallback = self.named_type("types.FunctionGenericAlias") + for name, typ in { + "__origin__": origin, + "__args__": tuple_type, + "__parameters__": tuple_type, + "__pep718_callable__": signature, + "__pep718_arguments__": arguments, + }.items(): + fallback = fallback.copy_with_extra_attr(name, typ) + return signature.copy_modified(fallback=fallback) def apply_type_arguments_to_callable( self, tp: Type, args: Sequence[Type], ctx: Context @@ -6721,7 +6840,9 @@ def has_abstract_type(self, caller_type: ProperType, callee_type: ProperType) -> and not self.chk.allow_abstract_call ) - def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> Type | None: + def try_parse_as_type_expression( + self, maybe_type_expr: Expression, *, allow_bound_type_vars: bool = False + ) -> Type | None: """Try to parse a value Expression as a type expression. If success then return the type that it spells. If fails then return None. @@ -6767,18 +6888,22 @@ def try_parse_as_type_expression(self, maybe_type_expr: Expression) -> Type | No } chk_sem = mypy.checker.TypeCheckerAsSemanticAnalyzer(self.chk, sym_for_name) + scope = TypeVarLikeScope() + if allow_bound_type_vars: + # Function subscription on a bound method/Callable value is + # recognized only here. Its arguments can reference enclosing + # type parameters that semantic analysis did not bind as types. + for enclosing in self.chk.scope.stack: + if isinstance(enclosing, TypeInfo): + for variable in enclosing.defn.type_vars: + scope.bind_existing(variable) + elif isinstance(enclosing, FuncItem): + signature = enclosing.type + if isinstance(signature, CallableType): + for variable in signature.variables: + scope.bind_existing(variable) tpan = TypeAnalyser( - chk_sem, - # NOTE: Will never need to lookup type vars in this scope because - # SemanticAnalyzer.try_parse_as_type_expression() will have - # already recognized any type var referenced in a NameExpr. - # String annotations (which may also reference type vars) - # can't be resolved in the TypeChecker pass anyway. - TypeVarLikeScope(), # empty scope - self.plugin, - self.chk.options, - self.chk.tree, - self.chk.is_typeshed_stub, + chk_sem, scope, self.plugin, self.chk.options, self.chk.tree, self.chk.is_typeshed_stub ) try: diff --git a/mypy/typeshed/stdlib/types.pyi b/mypy/typeshed/stdlib/types.pyi index ccd0d5930cddc..1cef2e655e6f0 100644 --- a/mypy/typeshed/stdlib/types.pyi +++ b/mypy/typeshed/stdlib/types.pyi @@ -711,6 +711,19 @@ class GenericAlias: # GenericAlias delegates attr access to `__origin__` def __getattr__(self, name: str) -> Any: ... +if sys.version_info >= (3, 16): + __all__ += ["FunctionGenericAlias"] + + class FunctionGenericAlias(GenericAlias): + __name__: str + __qualname__: str + @property + def __origin__(self) -> Any: ... + def __new__(cls, origin: Callable[..., Any], args: Any, /) -> Self: ... + def __getitem__(self, typeargs: Any, /) -> FunctionGenericAlias: ... + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... + + @final class NoneType: def __bool__(self) -> Literal[False]: ... diff --git a/test-data/unit/check-pep718.test b/test-data/unit/check-pep718.test index e353cac320101..d1a54113a1608 100644 --- a/test-data/unit/check-pep718.test +++ b/test-data/unit/check-pep718.test @@ -91,7 +91,7 @@ from typing import overload def pick[T](x: T) -> list[T]: ... @overload def pick[S, U](x: S, y: U) -> dict[S, U]: ... -def pick(x, y=None): ... +def pick[S, U](x: S, y: U | None = None): ... reveal_type(pick[int]) # N: Revealed type is "def (x: builtins.int) -> builtins.list[builtins.int]" reveal_type(pick[str, int]("a", 1)) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" pick[int, str, bytes] # E: No overload variant accepts 3 type arguments @@ -132,8 +132,8 @@ class IntSerializer: ... @overload def ser[T: str]() -> StringSerializer: ... @overload -def ser[T: int]() -> IntSerializer: ... # E: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader -def ser(): ... +def ser[T: int]() -> IntSerializer: ... +def ser[T: str | int](): ... reveal_type(ser[str]()) # N: Revealed type is "__main__.StringSerializer" reveal_type(ser[int]()) # N: Revealed type is "__main__.IntSerializer" ser[bytes]() # E: No overload variant accepts 1 type argument @@ -211,7 +211,7 @@ C.sm[int] # E: Function subscription requires Python 3.16 or newer def pick(x: T) -> T: ... @overload def pick(x: T, y: T) -> T: ... -def pick(x, y=None): ... +def pick[S, U](x: S, y: U | None = None): ... pick[int] # E: Function subscription requires Python 3.16 or newer def use_callable(f: Callable[[T], T]) -> None: f[int] # E: Function subscription requires Python 3.16 or newer @@ -223,3 +223,241 @@ class Indexable: def __getitem__(self, key: int) -> str: ... reveal_type(Indexable()[0]) # N: Revealed type is "builtins.str" [builtins fixtures/classmethod.pyi] + +[case testPEP718ReturnOnlyInference] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +from typing import TypeVar +T = TypeVar('T', default=int) +def factory[U]() -> U: ... +def defaulted() -> T: ... +def nested[U]() -> list[U]: ... +def phantom[U]() -> None: ... +reveal_type(factory[int]()) # N: Revealed type is "builtins.int" +x: int = factory() +y: list[str] = nested() +factory() # E: Cannot infer value of type parameter "U" of "factory" +nested() # E: Cannot infer value of type parameter "U" of "nested" +reveal_type(defaulted()) # N: Revealed type is "builtins.int" +phantom() +[builtins fixtures/list.pyi] + +[case testPEP718ReturnOnlyFlagDisabled] +# flags: --python-version=3.16 +def factory[T]() -> T: ... # E: A function returning TypeVar should receive at least one argument containing the same TypeVar +[builtins fixtures/tuple.pyi] + +[case testPEP718OverloadImplementationParameters] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +from typing import overload, TypeVar +T = TypeVar('T') +@overload +def f[T](x: T) -> T: ... +@overload +def f[T](x: T, y: int) -> T: ... +def f(x, y=0): ... # E: Overload implementation must declare type parameters when its overloads do +# Old-style overloads do not require a PEP 695 implementation. +@overload +def legacy(x: T) -> T: ... +@overload +def legacy(x: T, y: int) -> T: ... +def legacy(x, y=0): ... +[builtins fixtures/tuple.pyi] + +[case testPEP718OrdinaryOverloadErrors] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +from typing import overload +@overload +def f[T](x: T) -> T: ... +@overload +def f[T](x: T) -> T: ... # E: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader +def f[T](x: T) -> T: ... +[builtins fixtures/tuple.pyi] + +[case testPEP718MultipleVariadicsRejected] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +class Box[*Ts]: ... +def f[*Ts, *Us](x: Box[*Ts], y: Box[*Us]) -> None: ... +f[int, str](Box(), Box()) # E: Cannot subscript a function with multiple TypeVarTuples +[builtins fixtures/tuple.pyi] + +[case testPEP718MethodsWithoutOwnParameters] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +class C[T]: + def method(self, x: T) -> T: ... + @classmethod + def cm(cls, x: T) -> T: ... +C.method[int] # E: Cannot subscript a function with no type parameters +C.cm[int] # E: Cannot subscript a function with no type parameters +C[int]().method[str] # E: Cannot subscript a function with no type parameters +C[int].cm[str] # E: Cannot subscript a function with no type parameters +[builtins fixtures/classmethod.pyi] + +[case testPEP718ParamSpecShorthandAndValidation] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +from typing import Callable, ParamSpec +P = ParamSpec('P') +def legacy(f: Callable[P, int]) -> Callable[P, int]: ... +def modern[**P](f: Callable[P, int]) -> Callable[P, int]: ... +def wrap[**P, R](f: Callable[P, R]) -> Callable[P, R]: ... +reveal_type(legacy[int, str]) # N: Revealed type is "def (f: def (builtins.int, builtins.str) -> builtins.int) -> def (builtins.int, builtins.str) -> builtins.int" +reveal_type(modern[int, str]) # N: Revealed type is "def (f: def (builtins.int, builtins.str) -> builtins.int) -> def (builtins.int, builtins.str) -> builtins.int" +reveal_type(modern[[]]) # N: Revealed type is "def (f: def () -> builtins.int) -> def () -> builtins.int" +wrap[int, bool] # E: Can only replace ParamSpec with a parameter types list or another ParamSpec, got "int" +[builtins fixtures/tuple.pyi] + +[case testPEP718BoundMethodParamSpec] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +from typing import Callable +class C: + def wrap[**P, R](self, f: Callable[P, R]) -> Callable[P, R]: ... + def variadic[*Ts](self, *args: *Ts) -> tuple[*Ts]: ... +reveal_type(C().wrap[[int, str], bool]) # N: Revealed type is "def (f: def (builtins.int, builtins.str) -> builtins.bool) -> def (builtins.int, builtins.str) -> builtins.bool" +reveal_type(C().wrap[..., bool]) # N: Revealed type is "def (f: def (*Any, **Any) -> builtins.bool) -> def (*Any, **Any) -> builtins.bool" +reveal_type(C().variadic[*tuple[int, str]]) # N: Revealed type is "def (builtins.int, builtins.str) -> tuple[builtins.int, builtins.str]" +[builtins fixtures/tuple.pyi] + +[case testPEP718ParamSpecDecoratorReturnOnly] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +from typing import Callable +class Box[T]: ... +def decorate[**P, R](f: Callable[P, list[R]]) -> Callable[P, Box[R]]: ... +@decorate +def factory[T]() -> list[T]: ... +reveal_type(factory[int]()) # N: Revealed type is "__main__.Box[builtins.int]" +result: Box[str] = factory() +factory() # E: Cannot infer value of type parameter "T" of "factory" +[builtins fixtures/list.pyi] + +[case testPEP718AliasResubscription] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +def identity[T](x: T) -> T: ... +def use[T](x: T) -> None: + alias = identity[T] + reveal_type(alias[int]) # N: Revealed type is "def (x: builtins.int) -> builtins.int" + reveal_type(alias(x)) # N: Revealed type is "T`-1" + alias(1) # E: Argument 1 has incompatible type "int"; expected "T" +identity[int][str] # E: Cannot subscript a function with no type parameters +[builtins fixtures/tuple.pyi] + +[case testPEP718AliasRuntimeAttributes] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +import types +def f[T](x: T) -> T: ... +a = f[int] +reveal_type(a.__args__) # N: Revealed type is "builtins.tuple[Any, ...]" +reveal_type(a.__name__) # N: Revealed type is "builtins.str" +a.__origin__ +a.__parameters__ +reveal_type(a | None) # N: Revealed type is "types.UnionType" +assert isinstance(a, types.FunctionGenericAlias) +a('') # E: Argument 1 has incompatible type "str"; expected "int" +[builtins fixtures/tuple.pyi] +[file types.pyi] +from typing import Any +class UnionType: pass +class FunctionGenericAlias: + __origin__: Any + __args__: tuple[Any, ...] + __parameters__: tuple[Any, ...] + __name__: str + def __or__(self, other: Any) -> UnionType: ... + +[case testPEP718AliasIncremental] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +from library import alias +reveal_type(alias(1)) # N: Revealed type is "builtins.int" +alias('') # E: Argument 1 has incompatible type "str"; expected "int" +[builtins fixtures/tuple.pyi] +[file library.py] +def identity[T](x: T) -> T: ... +alias = identity[int] +[out2] +main:3: note: Revealed type is "builtins.int" +main:4: error: Argument 1 has incompatible type "str"; expected "int" + +[case testPEP718ContextManagerReturnOnly] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +from contextlib import contextmanager +from typing import Iterator +@contextmanager +def session[T]() -> Iterator[T]: ... +with session[int]() as value: + reveal_type(value) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] +[file contextlib.pyi] +from typing import Callable, ParamSpec, Iterator, Generic, TypeVar +_P = ParamSpec('_P') +_T = TypeVar('_T') +class Manager(Generic[_T]): + def __enter__(self) -> _T: ... + def __exit__(self, *args: object) -> None: ... +def contextmanager(func: Callable[_P, Iterator[_T]]) -> Callable[_P, Manager[_T]]: ... + +[case testPEP718AliasResubscriptionVariadics] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +from typing import Callable +def wrap[**P](f: Callable[P, int]) -> Callable[P, int]: ... +def variadic[*Ts](*args: *Ts) -> tuple[*Ts]: ... +def use[**P, *Ts](f: Callable[P, int], *args: *Ts) -> None: + alias = wrap[P] + reveal_type(alias[[int, str]]) # N: Revealed type is "def (f: def (builtins.int, builtins.str) -> builtins.int) -> def (builtins.int, builtins.str) -> builtins.int" + var_alias = variadic[*Ts] + reveal_type(var_alias[int, str]) # N: Revealed type is "def (builtins.int, builtins.str) -> tuple[builtins.int, builtins.str]" +[builtins fixtures/tuple.pyi] + +[case testPEP718OverloadConstraintsAndParamSpec] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +from typing import Callable, overload +@overload +def f[T: (str, bytes), **P](fn: Callable[P, None]) -> str: ... +@overload +def f[T: (int, float), **P](fn: Callable[P, None]) -> int: ... +def f[T, **P](fn: Callable[P, None]) -> str | int: ... +reveal_type(f[str, []]) # N: Revealed type is "def (fn: def ()) -> builtins.str" +reveal_type(f[int, []]) # N: Revealed type is "def (fn: def ()) -> builtins.int" +f[object, []] # E: No overload variant accepts 2 type arguments +[builtins fixtures/tuple.pyi] + +[case testPEP718AliasSQLiteIncremental] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 --sqlite-cache +from library import alias +reveal_type(alias(1)) # N: Revealed type is "builtins.int" +alias('') # E: Argument 1 has incompatible type "str"; expected "int" +[builtins fixtures/tuple.pyi] +[file library.py] +def identity[T](x: T) -> T: ... +alias = identity[int] +[out2] +main:3: note: Revealed type is "builtins.int" +main:4: error: Argument 1 has incompatible type "str"; expected "int" + + +[case testPEP718DecoratorPreservesDeclarationOrder] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +from typing import Callable +class Box[T]: ... +def decorate[**P, R](f: Callable[P, list[R]]) -> Callable[P, Box[R]]: ... +@decorate +def factory[T, U](arg: U) -> list[T]: ... +reveal_type(factory[int, str]) # N: Revealed type is "def (arg: builtins.str) -> __main__.Box[builtins.int]" +[builtins fixtures/list.pyi] + +[case testPEP718ReturnOnlyBoundAndConstraint] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +def bounded[T: str]() -> T: ... +def constrained[T: (str, int)]() -> T: ... +bounded() # E: Cannot infer value of type parameter "T" of "bounded" +constrained() # E: Cannot infer value of type parameter "T" of "constrained" +reveal_type(bounded[str]()) # N: Revealed type is "builtins.str" +reveal_type(constrained[int]()) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] + +[case testPEP718AliasResubscriptionClassInference] +# flags: --enable-incomplete-feature=SubscriptableFunctions --python-version=3.16 +class C[T]: + @classmethod + def cm[U](cls, x: T, y: U) -> tuple[T, U]: ... +def use[U]() -> None: + alias = C.cm[U] + reveal_type(alias[int](0, 1)) # N: Revealed type is "tuple[builtins.int, builtins.int]" +[builtins fixtures/classmethod.pyi] From ba731eb0b86deef4f965cb23b518d3a84396eaf4 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Sat, 24 Oct 2026 15:48:30 -0700 Subject: [PATCH 6/7] Fix worker import paths in JSON output tests --- mypy/test/testoutput.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mypy/test/testoutput.py b/mypy/test/testoutput.py index e18302cbf92a0..585f12869da73 100644 --- a/mypy/test/testoutput.py +++ b/mypy/test/testoutput.py @@ -9,7 +9,9 @@ import os import os.path import re +from unittest import mock +import mypy from mypy import api from mypy.defaults import PYTHON3_VERSION from mypy.test.config import test_temp_dir @@ -24,7 +26,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: def test_output_json(testcase: DataDrivenTestCase) -> None: - """Runs Mypy in a subprocess, and ensures that `--output=json` works as intended.""" + """Run mypy through the API and check its JSON output.""" program_text = "\n".join(testcase.input) flags_match = re.search("# flags: (.*)$", program_text, flags=re.MULTILINE) if flags_match is not None: @@ -42,7 +44,11 @@ def test_output_json(testcase: DataDrivenTestCase) -> None: output = [] # Type check the program. - out, err, returncode = api.run(mypy_cmdline) + # Workers run in the test's temporary directory, so make the same mypy + # installation as pytest available to their subprocesses. + mypy_path = os.path.dirname(os.path.abspath(mypy.__path__[0])) + with mock.patch.dict(os.environ, {"PYTHONPATH": mypy_path}): + out, err, returncode = api.run(mypy_cmdline) # split lines, remove newlines, and remove directory of test case for line in (out + err).rstrip("\n").splitlines(): if line.startswith(test_temp_dir + os.sep): From cdf8a97c31fad5c445aa02a5857e708ccb5d53cd Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Sat, 24 Oct 2026 16:13:13 -0700 Subject: [PATCH 7/7] Avoid triggering spelling error on 'set[int]' --- mypy/checkexpr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index b8e0e5a50bd06..249d84550964d 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -5402,8 +5402,8 @@ def apply_subscript_to_callable_item( ) # Silent mode (PEP 718 overload pre-filtering): a bound or constraint # violation means this item does not accept the subscription — e.g. - # `ser[int]` must skip an overload whose type parameter is bounded by - # str. Capture violations instead of reporting them. + # `serialize[int]` must skip an overload whose type parameter is bounded + # by `str`. Capture violations instead of reporting them. violations: list[str] = [] def note_violation(