typing.Unpack represents the static expansion of a type structure. It has two major uses: describing named arguments received through **kwargs from a TypedDict, and expanding a variadic tuple based on TypeVarTuple. In both cases, Unpack lets the checker see individual elements that would otherwise be hidden inside a generic collection.
This guide covers typed kwargs, required and optional options, forwarding wrappers, methods, factories, callable protocols, TypeVarTuple, variadic tuples, shape-preserving generics, runtime limitations, compatibility, and common mistakes.
The problem with generic **kwargs
def connect(**options: object) -> None:
...
connect(host="localhost", port=5432, ssl=True)The signature accepts any name and value. Editors cannot suggest options, misspelled keywords appear valid, and incorrect value types are difficult to detect.
TypedDict with Unpack
from typing import TypedDict, Unpack
class ConnectionOptions(TypedDict):
host: str
port: int
def connect(**options: Unpack[ConnectionOptions]) -> None:
host = options["host"]
port = options["port"]host and port are now required keyword arguments. The checker knows their value types and can reject unknown names.
Optional options
from typing import NotRequired
class ConnectionOptions(TypedDict):
host: str
port: int
timeout: NotRequired[float]
ssl: NotRequired[bool]Required and NotRequired determine which keywords callers must provide. The Python Required and NotRequired guide explains key presence in detail.
Valid and invalid calls
connect(host="db.local", port=5432)
connect(host="db.local", port=5432, timeout=3.0)
connect(host="db.local") # missing port
connect(host="db.local", port="5432") # wrong type
connect(host="db.local", port=5432, retries=2) # extra nameThese checks happen statically. At runtime, options is still an ordinary dictionary.
Inside the function
def connect(**options: Unpack[ConnectionOptions]) -> None:
timeout = options.get("timeout", 5.0)
use_ssl = options.get("ssl", False)Required keys may be indexed directly. Optional keys need a membership test, get(), or another default strategy.
Forwarding kwargs
def logged_connect(**options: Unpack[ConnectionOptions]) -> None:
print("connecting")
connect(**options)The wrapper preserves keyword names and types. Annotating the wrapper as **options: object would lose the relationship.
Adding explicit parameters
def logged_connect(
level: str,
**options: Unpack[ConnectionOptions],
) -> None:
...Normal parameters can appear before the unpacked kwargs. Avoid declaring an explicit parameter whose name also exists in the TypedDict.
Name conflicts
class Options(TypedDict):
level: str
# Do not combine an explicit level parameter with Unpack[Options]Each keyword must appear once in the effective signature. Type checkers should report overlaps between explicit parameters and unpacked keys.
Arbitrary extra kwargs
Unpack of a TypedDict normally describes a closed known set. If a function truly accepts arbitrary additional keywords, a TypedDict may not represent the API. Consider a separate metadata mapping, overloads, a configuration object, or an explicit extras parameter.
Callable protocols
from typing import Protocol
class EventData(TypedDict):
user_id: int
action: str
class EventCallback(Protocol):
def __call__(self, **data: Unpack[EventData]) -> None: ...A Protocol can express a callback that accepts specific named arguments. Implementations must provide a compatible call signature.
Unpack in methods
class Client:
def request(self, **options: Unpack[ConnectionOptions]) -> None:
...The behavior is the same for methods. self is separate from the expanded keyword set.
Object factories
class Configuration(TypedDict, total=False):
cache: bool
timeout: float
class Service:
def __init__(self, name: str, **config: Unpack[Configuration]) -> None:
...This provides autocomplete without a long parameter list. Explicit parameters are often clearer when the API is small and stable.
TypedDict inheritance
class BaseHttp(TypedDict, total=False):
timeout: float
headers: dict[str, str]
class GetOptions(BaseHttp, total=False):
params: dict[str, str]Unpack[GetOptions] includes inherited keys. Deep hierarchies make the effective signature hard to discover, so keep option types simple.
ReadOnly and kwargs
ReadOnly describes writes to TypedDict fields, but kwargs are newly constructed for every call. It is usually more valuable for persistent records than for option bags. Use it only when the semantics are clear and checker support is verified.
Unpack with TypeVarTuple
The second major use expands a variable-length sequence of type parameters:
from typing import Generic, TypeVarTuple, Unpack
Dimensions = TypeVarTuple("Dimensions")
class Array(Generic[Unpack[Dimensions]]):
...Specializations can contain different numbers of dimensions:
image: Array[int, int, int]
matrix: Array[int, int]The arguments represent an expanded tuple of types rather than a single tuple type.
Star syntax
Modern Python supports equivalent starred syntax in selected contexts:
class Array[*Dimensions]:
...Unpack[Dimensions] remains important for compatibility and contexts where starred syntax is unavailable.
Variadic tuples
Ts = TypeVarTuple("Ts")
def add_prefix(
values: tuple[Unpack[Ts]],
) -> tuple[str, Unpack[Ts]]:
return ("prefix", *values)The function preserves every positional element type from the original tuple while adding a string at the front.
Preserving shapes
Shape = TypeVarTuple("Shape")
class Tensor(Generic[Unpack[Shape]]):
...
def batch(x: Tensor[Unpack[Shape]]) -> Tensor[int, Unpack[Shape]]:
...Numerical libraries can model operations that preserve, add, or remove axes. This is a static relationship; it does not validate numeric dimensions at runtime.
One variadic group per parameter list
Multiple unconstrained TypeVarTuple groups would be ambiguous because a checker could not determine how to divide supplied arguments. Design APIs with fixed prefixes and suffixes around one variadic group.
TypeVarTuple versus tuple[T, …]
tuple[T, ...] means an arbitrary number of values sharing one type. TypeVarTuple can preserve different positional types:
tuple[int, str, bytes]
# Ts may represent (int, str, bytes)Unpack does not perform runtime work
The annotation does not unpack data, validate keywords, or change function calling. The normal ** and * operators still perform runtime expansion. Unpack describes that expansion to static tools.
Version compatibility
Use typing_extensions.Unpack and TypeVarTuple on older supported interpreters. Also verify the checker version because variadic generic support has improved over time.
Common mistakes
- Annotating **kwargs with a TypedDict but omitting Unpack: that means each keyword value is a TypedDict.
- Expecting runtime validation: kwargs remain a dict.
- Accepting arbitrary names without modeling them: Unpack describes known keys.
- Overlapping explicit and unpacked names: one keyword cannot appear twice.
- Confusing TypeVarTuple with a homogeneous tuple: it preserves distinct positions.
- Using variadics for simple APIs: explicit parameters may communicate better.
Complete example: HTTP client
from typing import TypedDict, Unpack
class HttpOptions(TypedDict, total=False):
timeout: float
headers: dict[str, str]
follow_redirects: bool
retries: int
def get(
url: str,
**options: Unpack[HttpOptions],
) -> bytes:
timeout = options.get("timeout", 10.0)
headers = options.get("headers", {})
follow = options.get("follow_redirects", True)
retries = options.get("retries", 1)
return perform_get(url, timeout, headers, follow, retries)Editors suggest the four option names, reject misspellings, and preserve value types. Runtime code receives a normal dictionary and applies defaults.
When to avoid Unpack
Prefer explicit parameters when there are only a few stable options. Use a configuration dataclass when settings are shared, validated, or passed through many layers. Use Unpack when the API is naturally based on keyword arguments and precision is worth preserving.
Conclusion
typing.Unpack makes expansions visible to the type system. With TypedDict, it creates named kwargs with known value types and presence rules. With TypeVarTuple, it enables generics with a variable number of positional type parameters.
The official Python Unpack documentation defines supported forms. Use it to preserve signatures and variadic relationships, while keeping runtime validation and choosing simpler explicit APIs whenever they are sufficient.







