2025, Nov 19 13:00

How to Type **kwargs in Python: prevent Pylance/pyright dict-unpacking errors with TypedDict extra_items and strictDictionaryInference

Learn why Pylance/pyright warn on dict-unpacking into **kwargs and how to fix it with TypedDict extra_items and strictDictionaryInference. Clear patterns.

Keyword arguments, static types and dict-unpacking often meet in one place: a function that accepts both a typed keyword parameter and a catch‑all **kwargs. When a type checker like Pylance or pyright enters the scene, seemingly harmless calls can trigger confusing diagnostics. Below is a compact walkthrough of why this happens and how to structure your types so that tooling stays helpful instead of noisy.

Reproducing the issue

The function accepts an optional boolean plus any number of int-valued extras. Unpacking a dict into it looks straightforward, yet Pylance flags it.

def process(flag_optional: bool = False, **extras: int):
    pass
process(**{"some_input": 5})  # Pylance error

Swapping the optional for a required parameter exposes another twist. The following call slides through under default settings, even though the value for the boolean is not a bool.

def process_required(flag_required: bool, **extras: int):
    pass
# Accepted under default inference, but semantically wrong
process_required(**{"flag_required": 5, "some_input": True})

Why the checker complains

The core of the first warning is potential assignment. A dict annotated or inferred as dict[str, int] could include a key that matches the function’s boolean keyword parameter. Because that key might be present, a type checker must assume it could pass an int where a bool is expected. The fact that the boolean parameter is optional doesn’t change anything here; it’s still a keyword parameter and the unpacked mapping could supply it.

The second behavior comes from pyright’s default dictionary inference. A literal like {"some_input": True, "flag_required": 2} is inferred as dict[str, Unknown]. Unknown behaves like Any and silently accommodates mismatches, so the call above passes without a type error. Enabling the pyright option strictDictionaryInference=true changes the inferred type to dict[str, bool | int], which in turn triggers a warning that an int might be assigned to a bool.

What to do about it

One option is to route the boolean through a positional-only parameter and constrain **kwargs to exactly dict[str, int]. This keeps types tight but changes the calling surface, which is not always desirable if you intentionally want to supply the non-kwargs argument via kwargs. It is possible to pop the boolean out of the mapping and pass it positionally while forwarding the rest, although you may find yourself casting to appease the checker, which risks masking other type issues.

def handle(mode: bool = False, /, **stash: int):
    pass
# Extract from mapping, pass the boolean positionally, forward the ints
# handle(cast(bool, items.pop("mode", False)), **cast(dict[str, int], items))

A more expressive way, supported by pyright, is to describe the unpacked mapping with a TypedDict that fixes the special key’s type while allowing extra integer items. This relies on the experimental extra_items feature.

# pyright: enableExperimentalFeatures=true
from typing_extensions import TypedDict, NotRequired
class Payload(TypedDict, extra_items=int):
    flag_optional: NotRequired[bool]
def process(flag_optional: bool = False, **extras: int):
    pass
args: Payload = {"some_input": 5}  # OK
process(**args)  # OK

This feature is introduced with PEP 728 and probably comes with Python 3.15. Until then, it is available via typing_extensions; pyright in its latest versions supports it already.

If the boolean is required, the same pattern catches incorrect values on unpack:

class PayloadRequired(TypedDict, extra_items=int):
    flag_required: bool
def process_required(flag_required: bool, **extras: int):
    pass
process_required(**PayloadRequired({"flag_required": 5, "some_input": True}))  # Type error for flag_required

Why this matters

Dict-unpacking into functions is a common integration path between dynamic data and typed APIs. If the mapping’s shape isn’t modeled precisely, either you get false positives that block clean code or false negatives that hide real defects. Tightening dictionary inference or, better, using a TypedDict with extra_items aligns the checker’s view with the way the function is meant to be called, keeping diagnostics actionable.

Takeaways

If you depend on passing a non-kwargs parameter via kwargs, positional-only parameters won’t fit your design. In that case, prefer a TypedDict with extra_items=int to make the special boolean key explicit while allowing additional integer fields, and enable experimental support in pyright. If you’re hitting silent acceptance of bad calls, turn on strictDictionaryInference=true so that mixed-value dict literals don’t degrade to dict[str, Unknown]. These adjustments keep your code flexible at the call site and precise in the type system without resorting to scattered # type: ignore.