diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 8288b676b52ea..667b9d66f2153 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -209,6 +209,7 @@ is_overlapping_none, is_self_type_like, remove_optional, + try_getting_literal, ) from mypy.typestate import type_state from mypy.typevars import fill_typevars @@ -1764,9 +1765,21 @@ def check_callable_call( need_refresh = any( isinstance(v, (ParamSpecType, TypeVarTupleType)) for v in callee.variables ) + # Check if we have TypeVar-based kwargs that need expansion after inference + has_typevar_kwargs = ( + callee.unpack_kwargs + and callee.arg_types + and isinstance(callee.arg_types[-1], UnpackType) + and isinstance(get_proper_type(callee.arg_types[-1].type), TypeVarType) + ) callee = self.infer_function_type_arguments( callee, args, arg_kinds, arg_names, formal_to_actual, need_refresh, context ) + if has_typevar_kwargs: + # After inference, the TypeVar in **kwargs should be replaced with + # an inferred TypedDict. Re-expand the kwargs now. + callee = callee.with_unpacked_kwargs().with_normalized_var_args() + need_refresh = True if need_refresh: # Argument kinds etc. may have changed due to # ParamSpec or TypeVarTuple variables being replaced with an arbitrary @@ -6833,14 +6846,6 @@ def merge_typevars_in_callables_by_name( return output, variables -def try_getting_literal(typ: Type) -> ProperType: - """If possible, get a more precise literal type for a given type.""" - typ = get_proper_type(typ) - if isinstance(typ, Instance) and typ.last_known_value is not None: - return typ.last_known_value - return typ - - def is_expr_literal_type(node: Expression) -> bool: """Returns 'true' if the given node is a Literal""" if isinstance(node, IndexExpr): diff --git a/mypy/constraints.py b/mypy/constraints.py index df79fdae5456c..f95f140aeeac6 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -58,7 +58,7 @@ is_named_instance, split_with_prefix_and_suffix, ) -from mypy.types_utils import is_union_with_any +from mypy.types_utils import is_union_with_any, try_getting_literal from mypy.typestate import type_state if TYPE_CHECKING: @@ -135,7 +135,7 @@ def infer_constraints_for_callable( break for i, actuals in enumerate(formal_to_actual): - if isinstance(callee.arg_types[i], UnpackType): + if isinstance(callee.arg_types[i], UnpackType) and callee.arg_kinds[i] == ARG_STAR: unpack_type = callee.arg_types[i] assert isinstance(unpack_type, UnpackType) @@ -218,6 +218,88 @@ def infer_constraints_for_callable( constraints.extend(infer_constraints(tt, at, SUPERTYPE_OF)) else: assert False, "mypy bug: unhandled constraint inference case" + + elif isinstance(callee.arg_types[i], UnpackType) and callee.arg_kinds[i] == ARG_STAR2: + # Handle **kwargs: Unpack[K] where K is TypeVar bound to TypedDict. + # Collect actual kwargs and build a TypedDict constraint. + + unpack_type = callee.arg_types[i] + assert isinstance(unpack_type, UnpackType) + + unpacked_type = get_proper_type(unpack_type.type) + assert isinstance(unpacked_type, TypeVarType) + + other_named = { + name + for name, kind in zip(callee.arg_names, callee.arg_kinds) + if name is not None and not kind.is_star() + } + + # Collect all the arguments that will go to **kwargs + kwargs_items: dict[str, Type] = {} + for actual in actuals: + actual_arg_type = arg_types[actual] + if actual_arg_type is None: + continue + actual_name = arg_names[actual] if arg_names is not None else None + if actual_name is not None: + # Named argument going to **kwargs + kwargs_items[actual_name] = actual_arg_type + elif arg_kinds[actual] == ARG_STAR2: + # **kwargs being passed through - try to extract TypedDict items + p_actual = get_proper_type(actual_arg_type) + if isinstance(p_actual, TypedDictType): + for sname, styp in p_actual.items.items(): + # But we need to filter out names that + # will go to other parameters + if sname not in other_named: + kwargs_items[sname] = styp + + # Build a TypedDict from the collected kwargs. + bound = get_proper_type(unpacked_type.upper_bound) + if isinstance(bound, Instance) and bound.type.typeddict_type is not None: + bound = bound.type.typeddict_type + + # This should be an error from an earlier level, but don't compound it + if not isinstance(bound, TypedDictType): + continue + + # Start with the actual kwargs passed, with literal types + # inferred for read-only and unbound items + items = { + key: ( + try_getting_literal(typ) + if key not in bound.items or key in bound.readonly_keys + else typ + ) + for key, typ in kwargs_items.items() + } + # Add any NotRequired keys from the bound that weren't passed + # (they need to be present for TypedDict subtyping to work) + for key, value_type in bound.items.items(): + if key not in items and key not in bound.required_keys: + # If the key is missing and it is ReadOnly, + # then we can replace the type with Never to + # indicate that it is definitely not + # present. We can't do that if it is mutable, + # though (because that violates the subtyping + # rules.) + items[key] = ( + value_type if key not in bound.readonly_keys else UninhabitedType() + ) + # Keys are required if they're required in the bound, or if they're + # extra keys not in the bound (explicitly passed, so required). + required_keys = { + key for key in items if key in bound.required_keys or key not in bound.items + } + inferred_td = TypedDictType( + items=items, + required_keys=required_keys, + readonly_keys=bound.readonly_keys, + fallback=bound.fallback, + ) + constraints.append(Constraint(unpacked_type, SUPERTYPE_OF, inferred_td)) + else: for actual in actuals: actual_arg_type = arg_types[actual] diff --git a/mypy/semanal.py b/mypy/semanal.py index f38a71cb16e30..e5e4501c3fe5d 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1103,8 +1103,26 @@ def remove_unpack_kwargs(self, defn: FuncDef, typ: CallableType) -> CallableType if not isinstance(last_type, UnpackType): return typ p_last_type = get_proper_type(last_type.type) + + # Handle TypeVar bound to TypedDict - allows inferring TypedDict from kwargs + if isinstance(p_last_type, TypeVarType): + bound = get_proper_type(p_last_type.upper_bound) + if not self.is_typeddict_like(bound): + self.fail( + "Unpack item in ** parameter must be a TypedDict or a TypeVar with TypedDict bound", + last_type, + ) + new_arg_types = typ.arg_types[:-1] + [AnyType(TypeOfAny.from_error)] + return typ.copy_modified(arg_types=new_arg_types) + # For TypeVar, we can't check overlap statically since the actual TypedDict + # will be inferred at call sites. Keep the TypeVar for constraint inference. + return typ.copy_modified(unpack_kwargs=True) + if not isinstance(p_last_type, TypedDictType): - self.fail("Unpack item in ** parameter must be a TypedDict", last_type) + self.fail( + "Unpack item in ** parameter must be a TypedDict or a TypeVar with TypedDict bound", + last_type, + ) new_arg_types = typ.arg_types[:-1] + [AnyType(TypeOfAny.from_error)] return typ.copy_modified(arg_types=new_arg_types) overlap = set(typ.arg_names) & set(p_last_type.items) @@ -1119,6 +1137,23 @@ def remove_unpack_kwargs(self, defn: FuncDef, typ: CallableType) -> CallableType new_arg_types = typ.arg_types[:-1] + [p_last_type] return typ.copy_modified(arg_types=new_arg_types, unpack_kwargs=True) + def is_typeddict_like(self, typ: ProperType) -> bool: + """Check if type is TypedDict or inherits from BaseTypedDict.""" + if isinstance(typ, TypedDictType): + return True + if isinstance(typ, Instance): + # Check if it's a TypedDict class or inherits from BaseTypedDict + if typ.type.typeddict_type is not None: + return True + for base in typ.type.mro: + if base.fullname in ( + "typing.TypedDict", + "typing.BaseTypedDict", + "_typeshed.typemap.BaseTypedDict", + ): + return True + return False + def prepare_method_signature(self, func: FuncDef, info: TypeInfo, has_self_type: bool) -> None: """Check basic signature validity and tweak annotation of self/cls argument.""" # Only non-static methods are special, as well as __new__. diff --git a/mypy/semanal_typeargs.py b/mypy/semanal_typeargs.py index 0f62a4aa8b1a2..6e13c60244849 100644 --- a/mypy/semanal_typeargs.py +++ b/mypy/semanal_typeargs.py @@ -28,6 +28,7 @@ TupleType, Type, TypeAliasType, + TypedDictType, TypeOfAny, TypeVarLikeType, TypeVarTupleType, @@ -255,6 +256,16 @@ def visit_unpack_type(self, typ: UnpackType) -> None: # tricky however, since this needs map_instance_to_supertype() available in many places. if isinstance(proper_type, Instance) and proper_type.type.fullname == "builtins.tuple": return + # TypeVar with TypedDict bound is allowed for **kwargs unpacking with inference. + # Note: for concrete TypedDict, semanal.py's remove_unpack_kwargs() unwraps the Unpack, + # so this check won't be reached. For TypeVar, we keep the Unpack for constraint inference. + if isinstance(proper_type, TypeVarType): + bound = get_proper_type(proper_type.upper_bound) + if isinstance(bound, TypedDictType): + return + # Also allow Instance bounds that are TypedDict-like + if isinstance(bound, Instance) and bound.type.typeddict_type is not None: + return if not isinstance(proper_type, (UnboundType, AnyType)): # Avoid extra errors if there were some errors already. Also interpret plain Any # as tuple[Any, ...] (this is better for the code in type checker). diff --git a/mypy/types.py b/mypy/types.py index db8fd4605659c..27165fdd3651a 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2467,6 +2467,19 @@ def with_unpacked_kwargs(self) -> NormalizedCallableType: if not self.unpack_kwargs: return cast(NormalizedCallableType, self) last_type = get_proper_type(self.arg_types[-1]) + # Handle Unpack[K] where K is TypeVar bound to TypedDict + if isinstance(last_type, UnpackType): + unpacked = get_proper_type(last_type.type) + if isinstance(unpacked, TypeVarType): + # TypeVar with TypedDict bound - can't expand until after inference. + # Return unchanged for now; expansion happens after type var substitution. + return cast(NormalizedCallableType, self) + # For TypedDict inside UnpackType, unwrap it + if isinstance(unpacked, TypedDictType): + last_type = unpacked + if isinstance(last_type, TypeVarType): + # Direct TypeVar (shouldn't happen normally but handle it) + return cast(NormalizedCallableType, self) assert isinstance(last_type, TypedDictType) extra_kinds = [ ArgKind.ARG_NAMED if name in last_type.required_keys else ArgKind.ARG_NAMED_OPT diff --git a/mypy/types_utils.py b/mypy/types_utils.py index 3c1dcb427f29a..b07c31f4faab7 100644 --- a/mypy/types_utils.py +++ b/mypy/types_utils.py @@ -177,4 +177,18 @@ def store_argument_type( elif typ.arg_kinds[i] == ARG_STAR2: if not isinstance(arg_type, ParamSpecType) and not typ.unpack_kwargs: arg_type = named_type("builtins.dict", [named_type("builtins.str", []), arg_type]) + # Strip the Unpack from Unpack[K], since it isn't part of the + # type inside the function + elif isinstance(arg_type, UnpackType): + unpacked_type = get_proper_type(arg_type.type) + assert isinstance(unpacked_type, TypeVarType) + arg_type = unpacked_type defn.arguments[i].variable.type = arg_type + + +def try_getting_literal(typ: Type) -> ProperType: + """If possible, get a more precise literal type for a given type.""" + typ = get_proper_type(typ) + if isinstance(typ, Instance) and typ.last_known_value is not None: + return typ.last_known_value + return typ diff --git a/test-data/unit/check-kwargs-unpack-typevar.test b/test-data/unit/check-kwargs-unpack-typevar.test new file mode 100644 index 0000000000000..f26803795bde0 --- /dev/null +++ b/test-data/unit/check-kwargs-unpack-typevar.test @@ -0,0 +1,266 @@ +[case testUnpackTypeVarKwargsBasicAccepted] +# flags: --python-version 3.12 +# Test that TypeVar with TypedDict bound is accepted in **kwargs +from typing import TypedDict, Unpack + +class BaseTypedDict(TypedDict): + pass + +def f[K: BaseTypedDict](**kwargs: Unpack[K]) -> K: + return kwargs + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testUnpackTypeVarKwargsInvalidBoundInt] +from typing import TypeVar, Unpack + +T = TypeVar('T', bound=int) + +def f(**kwargs: Unpack[T]) -> None: # E: Unpack item in ** parameter must be a TypedDict or a TypeVar with TypedDict bound + pass + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testUnpackTypeVarKwargsNoBound] +from typing import TypeVar, Unpack + +T = TypeVar('T') + +def f(**kwargs: Unpack[T]) -> None: # E: Unpack item in ** parameter must be a TypedDict or a TypeVar with TypedDict bound + pass + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testUnpackTypeVarKwargsConcreteTypedDictStillWorks] +# Test that concrete TypedDict still works as before +from typing import TypedDict, Unpack + +class TD(TypedDict): + x: int + y: str + +def f(**kwargs: Unpack[TD]) -> None: + pass + +f(x=1, y="hello") +f(x=1) # E: Missing named argument "y" for "f" + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testUnpackTypeVarKwargsInferBasic] +# flags: --python-version 3.12 +# Test that kwargs TypeVar inference works +from typing import TypedDict, Unpack + +class BaseTypedDict(TypedDict): + pass + +def f[K: BaseTypedDict](**kwargs: Unpack[K]) -> K: + return kwargs + +result = f(x=1, y="hello") +reveal_type(result) # N: Revealed type is "TypedDict('__main__.BaseTypedDict', {'x': Literal[1], 'y': Literal['hello']})" + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testUnpackTypeVarKwargsInferEmpty] +# flags: --python-version 3.12 +# Test empty kwargs infers empty TypedDict +from typing import TypedDict, Unpack + +class BaseTypedDict(TypedDict): + pass + +def f[K: BaseTypedDict](**kwargs: Unpack[K]) -> K: + return kwargs + +result = f() +reveal_type(result) # N: Revealed type is "TypedDict('__main__.BaseTypedDict', {})" + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testUnpackTypeVarKwargsWithPositionalParam] +# flags: --python-version 3.12 +# Test with positional parameter +from typing import TypedDict, Unpack + +class BaseTypedDict(TypedDict): + pass + +def g[K: BaseTypedDict](a: int, **kwargs: Unpack[K]) -> tuple[int, K]: + return (a, kwargs) + +result = g(1, name="test", count=42) +reveal_type(result) # N: Revealed type is "tuple[builtins.int, TypedDict('__main__.BaseTypedDict', {'name': Literal['test'], 'count': Literal[42]})]" + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testUnpackTypeVarKwargsNotGoingToKwargs] +# flags: --python-version 3.12 +# Test that explicit keyword params don't go to kwargs TypeVar +from typing import TypedDict, Unpack + +class BaseTypedDict(TypedDict): + pass + +def h[K: BaseTypedDict](*, required: str, **kwargs: Unpack[K]) -> K: + return kwargs + +# 'required' goes to explicit param, only 'extra' goes to kwargs +result = h(required="yes", extra=42) +reveal_type(result) # N: Revealed type is "TypedDict('__main__.BaseTypedDict', {'extra': Literal[42]})" + +# Only explicit params, no extra kwargs +result2 = h(required="yes") +reveal_type(result2) # N: Revealed type is "TypedDict('__main__.BaseTypedDict', {})" + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testUnpackTypeVarKwargsBoundWithRequiredFields] +# flags: --python-version 3.12 +# Test that bound TypedDict fields are required +from typing import TypedDict, Unpack + +class BaseTD(TypedDict): + x: int + +def f[K: BaseTD](**kwargs: Unpack[K]) -> K: + return kwargs + +# Missing required field 'x' from the bound - inferred TypedDict doesn't satisfy bound +f() # E: Value of type variable "K" of "f" cannot be "BaseTD" +f(y="hello") # E: Value of type variable "K" of "f" cannot be "BaseTD" + +# Providing 'x' satisfies the bound +result1 = f(x=1) +reveal_type(result1) # N: Revealed type is "TypedDict('__main__.BaseTD', {'x': builtins.int})" + +# Extra fields are allowed and inferred +result2 = f(x=1, y="hello", z=True) +reveal_type(result2) # N: Revealed type is "TypedDict('__main__.BaseTD', {'x': builtins.int, 'y': Literal['hello'], 'z': Literal[True]})" + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testUnpackTypeVarKwargsBoundWithNotRequired1] +# flags: --python-version 3.12 +# Test that NotRequired fields from bound can be omitted +from typing import TypedDict, NotRequired, Unpack + +class BaseTDWithOptional(TypedDict): + x: int + y: NotRequired[str] + +def g[K: BaseTDWithOptional](**kwargs: Unpack[K]) -> K: + return kwargs + +# Can omit NotRequired field 'y' +result1 = g(x=1) +reveal_type(result1) # N: Revealed type is "TypedDict('__main__.BaseTDWithOptional', {'x': builtins.int, 'y'?: builtins.str})" + +# Can provide NotRequired field 'y' +result2 = g(x=1, y="hello") +reveal_type(result2) # N: Revealed type is "TypedDict('__main__.BaseTDWithOptional', {'x': builtins.int, 'y'?: builtins.str})" + +# Still need required field 'x' +g(y="hello") # E: Value of type variable "K" of "g" cannot be "BaseTDWithOptional" + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + +[case testUnpackTypeVarKwargsBoundWithNotRequired2] +# flags: --python-version 3.12 +# Test that NotRequired fields from bound can be omitted +from typing import TypedDict, NotRequired, ReadOnly, Unpack + +class BaseTDWithOptional(TypedDict): + x: ReadOnly[int] + y: ReadOnly[NotRequired[str]] + +def g[K: BaseTDWithOptional](**kwargs: Unpack[K]) -> K: + return kwargs + +# Can omit NotRequired field 'y' +result1 = g(x=1) +reveal_type(result1) # N: Revealed type is "TypedDict('__main__.BaseTDWithOptional', {'x'=: Literal[1], 'y'?=: Never})" + +# Can provide NotRequired field 'y' +result2 = g(x=1, y="hello") +reveal_type(result2) # N: Revealed type is "TypedDict('__main__.BaseTDWithOptional', {'x'=: Literal[1], 'y'?=: Literal['hello']})" + +# Still need required field 'x' +g(y="hello") # E: Value of type variable "K" of "g" cannot be "BaseTDWithOptional" + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + + +[case testUnpackTypeVarKwargsBasicMixed] +# flags: --python-version 3.12 +# Test that TypeVar with TypedDict bound is accepted in **kwargs +from typing import TypedDict, Unpack + +class BaseTypedDict(TypedDict): + pass + +class Args(TypedDict): + x: int + y: str + + +def f[K: BaseTypedDict](x: int, **kwargs: Unpack[K]) -> K: + return kwargs + + +kwargs: Args +reveal_type(f(**kwargs)) # N: Revealed type is "TypedDict('__main__.BaseTypedDict', {'y': builtins.str})" + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] + + +[case testUnpackTypeVarKwargsInitField] +# flags: --python-version 3.12 +# Test that TypeVar with TypedDict bound is accepted in **kwargs +from typing import TypedDict, Unpack + +class BaseTypedDict(TypedDict): + pass + +class Args(TypedDict): + x: int + + +class InitField[KwargDict: BaseTypedDict]: + def __init__(self, **kwargs: Unpack[KwargDict]) -> None: + ... + + +class Field[KwargDict: Args](InitField[KwargDict]): + pass + +# XXX: mypy produces instances with last_known_values displayed with +# ?s if not assigned to a value?? +# Though, +# TODO: Do this on purpose?? +x = InitField(x=10, y='lol') +reveal_type(x) # N: Revealed type is "__main__.InitField[TypedDict('__main__.BaseTypedDict', {'x': Literal[10], 'y': Literal['lol']})]" + +a = Field(x=10, y='lol') +reveal_type(a) # N: Revealed type is "__main__.Field[TypedDict('__main__.Args', {'x': builtins.int, 'y': Literal['lol']})]" + +# TODO: These error messages are terrible and also wrong +Field(y='lol') # E: Value of type variable "KwargDict" of "Field" cannot be "Args" +Field(x='asdf') # E: Value of type variable "KwargDict" of "Field" cannot be "Args" + + +[builtins fixtures/dict.pyi] +[typing fixtures/typing-full.pyi] diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 874c79df0e193..9bdaa7953f206 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2191,7 +2191,7 @@ g(1, 2, 3) # E: Missing named argument "a" for "g" \ def bad( *args: Unpack[Keywords], # E: "Keywords" cannot be unpacked (must be tuple or TypeVarTuple) - **kwargs: Unpack[Ints], # E: Unpack item in ** parameter must be a TypedDict + **kwargs: Unpack[Ints], # E: Unpack item in ** parameter must be a TypedDict or a TypeVar with TypedDict bound ) -> None: ... reveal_type(bad) # N: Revealed type is "def (*args: Any, **kwargs: Any)" @@ -2199,7 +2199,7 @@ def bad2( one: int, *args: Unpack[Keywords], # E: "Keywords" cannot be unpacked (must be tuple or TypeVarTuple) other: str = "no", - **kwargs: Unpack[Ints], # E: Unpack item in ** parameter must be a TypedDict + **kwargs: Unpack[Ints], # E: Unpack item in ** parameter must be a TypedDict or a TypeVar with TypedDict bound ) -> None: ... reveal_type(bad2) # N: Revealed type is "def (one: builtins.int, *args: Any, other: builtins.str =, **kwargs: Any)" [builtins fixtures/dict.pyi] diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test index 4f45e61a6b6e2..a827ed1f30f55 100644 --- a/test-data/unit/check-varargs.test +++ b/test-data/unit/check-varargs.test @@ -792,7 +792,7 @@ def baz(**kwargs: Unpack[Person]) -> None: # OK [case testUnpackWithoutTypedDict] from typing_extensions import Unpack -def foo(**kwargs: Unpack[dict]) -> None: # E: Unpack item in ** parameter must be a TypedDict +def foo(**kwargs: Unpack[dict]) -> None: # E: Unpack item in ** parameter must be a TypedDict or a TypeVar with TypedDict bound ... [builtins fixtures/dict.pyi] diff --git a/test-data/unit/fixtures/typing-full.pyi b/test-data/unit/fixtures/typing-full.pyi index 87b66c0cd857e..3d5a9cd70f573 100644 --- a/test-data/unit/fixtures/typing-full.pyi +++ b/test-data/unit/fixtures/typing-full.pyi @@ -38,6 +38,9 @@ TypedDict = 0 TypeGuard = 0 NoReturn = 0 NewType = 0 +Required = 0 +NotRequired = 0 +ReadOnly = 0 Self = 0 Unpack = 0 Callable: _SpecialForm diff --git a/test-data/unit/semanal-errors.test b/test-data/unit/semanal-errors.test index b69d35ce030e7..f61b1ffdcfc0f 100644 --- a/test-data/unit/semanal-errors.test +++ b/test-data/unit/semanal-errors.test @@ -1471,7 +1471,7 @@ class Variadic(Generic[Unpack[TVariadic], Unpack[TVariadic2]]): # E: Can only u def bad_args(*args: TVariadic): # E: TypeVarTuple "TVariadic" is only valid with an unpack pass -def bad_kwargs(**kwargs: Unpack[TVariadic]): # E: Unpack item in ** parameter must be a TypedDict +def bad_kwargs(**kwargs: Unpack[TVariadic]): # E: Unpack item in ** parameter must be a TypedDict or a TypeVar with TypedDict bound pass [builtins fixtures/dict.pyi]