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

    Python code execution and performance monitoring
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    sys.monitoring: Low-Overhead Instrumentation

    Learn Python sys.monitoring for low-overhead instrumentation with selective events, callbacks, tooling, and safe observability.

    Ler mais

    Tempo de leitura: 5 minutos
    03/09/2026
    Software developer organizing object data with Python operator.attrgetter
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    operator.attrgetter: Sort Objects by Attributes

    Learn Python operator.attrgetter to sort, group, and transform objects by simple or nested attributes with clearer reusable code.

    Ler mais

    Tempo de leitura: 4 minutos
    02/09/2026
    Asynchronous programming with Python asyncio.Runner
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    asyncio.Runner: Reuse the Event Loop Safely

    Learn Python asyncio.Runner to reuse an event loop, control context, signals, debug mode, cancellation, and safe asynchronous shutdown.

    Ler mais

    Tempo de leitura: 6 minutos
    02/09/2026
    Binary data compression with Zstandard in Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    compression.zstd: Zstandard Streams and Dictionaries

    Learn Python compression.zstd for Zstandard compression, streaming, dictionaries, safe limits, testing, and production workflows.

    Ler mais

    Tempo de leitura: 6 minutos
    01/09/2026
    Python application packaged as an executable zipapp archive
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python zipapp: Build Executable Apps

    Learn Python zipapp to package applications as executable pyz archives, include dependencies, and distribute tools safely.

    Ler mais

    Tempo de leitura: 5 minutos
    01/09/2026
    Python code used to compose functions with functools.Placeholder
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    functools.Placeholder: Positional Gaps in partial

    Learn Python functools.Placeholder to leave positional gaps in partial functions and build clearer reusable functional APIs.

    Ler mais

    Tempo de leitura: 5 minutos
    31/08/2026