Python Unpack: Typed kwargs and Variadics

Published on: August 29, 2026
Reading time: 4 minutes
A person typing on a laptop with a Python programming book visible, capturing technology and learning.

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 name

These 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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python runtime_checkable: Protocol at Runtime

    Learn Python runtime_checkable to test Protocols with isinstance, understand its limits, and design safer structural contracts.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Required and NotRequired: Optional TypedDict Keys

    Learn Python Required and NotRequired to control mandatory and optional TypedDict keys without confusing absence with None.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ReadOnly: Protect TypedDict Fields

    Learn Python ReadOnly to protect TypedDict fields, model stable response contracts, and prevent accidental writes during static analysis.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    Close-up of hands typing on a laptop keyboard, Python book in sight, coding in progress.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python TypeAliasType: Runtime Type Aliases

    Learn Python TypeAliasType to create explicit aliases, inspect them at runtime, and model reusable generic APIs safely.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    Detailed view of programming code in a dark theme on a computer screen.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python overload: Precise Function Signatures

    Learn Python overload for precise signatures with Literal, None, generics, methods, and return types that depend on arguments.

    Ler mais

    Tempo de leitura: 6 minutos
    29/08/2026
    A person reads 'Python for Unix and Linux System Administration' indoors.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ClassVar: Separate Class and Instance State

    Learn Python ClassVar to separate class and instance state in dataclasses, registries, caches, inheritance, and counters.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026