Implement Unpack of TypeVars for **kwargs inference
Allow `**kwargs: Unpack[K]` where K is a TypeVar bound to TypedDict.
This enables inferring TypedDict types from keyword arguments at call sites.
Example:
def f[K: BaseTypedDict](**kwargs: Unpack[K]) -> K: ...
result = f(x=1, y="hello") # K inferred as TypedDict({'x': int, 'y': str})
Changes:
- semanal.py: Accept TypeVar with TypedDict bound in remove_unpack_kwargs(),
keep UnpackType for constraint inference
- semanal_typeargs.py: Allow TypeVar with TypedDict bound in visit_unpack_type()
- constraints.py: Generate TypedDict constraints from actual kwargs
- types.py: Handle TypeVar in with_unpacked_kwargs()
- checkexpr.py: Re-expand kwargs after TypeVar inference
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
index 8288b67..667b9d6 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 @@
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 @@
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 df79fda..f95f140 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 @@
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 @@
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 f38a71c..e5e4501 100644
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -1103,8 +1103,26 @@
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 @@
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 0f62a4a..6e13c60 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 @@
# 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 db8fd46..27165fd 100644
--- a/mypy/types.py
+++ b/mypy/types.py
@@ -2467,6 +2467,19 @@
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 3c1dcb4..b07c31f 100644
--- a/mypy/types_utils.py
+++ b/mypy/types_utils.py
@@ -177,4 +177,18 @@
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 0000000..f268037
--- /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 874c79d..9bdaa79 100644
--- a/test-data/unit/check-typevar-tuple.test
+++ b/test-data/unit/check-typevar-tuple.test
@@ -2191,7 +2191,7 @@
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 @@
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 4f45e61..a827ed1 100644
--- a/test-data/unit/check-varargs.test
+++ b/test-data/unit/check-varargs.test
@@ -792,7 +792,7 @@
[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 87b66c0..3d5a9cd 100644
--- a/test-data/unit/fixtures/typing-full.pyi
+++ b/test-data/unit/fixtures/typing-full.pyi
@@ -38,6 +38,9 @@
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 b69d35c..f61b1ff 100644
--- a/test-data/unit/semanal-errors.test
+++ b/test-data/unit/semanal-errors.test
@@ -1471,7 +1471,7 @@
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]