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]