2025, Nov 28 12:02

Именованные аргументы и **kwargs в Python: как задать типы с TypedDict extra_items

Почему Pylance/pyright ругаются на распаковку kwargs и как это исправить: TypedDict с extra_items, strictDictionaryInference и позиционные‑только параметры.

Именованные аргументы, статическая типизация и распаковка словарей часто сходятся в одной точке: в функции, которая принимает и типизированный именованный параметр, и «собирательные» **kwargs. Как только подключается типизатор вроде Pylance или pyright, безобидные на вид вызовы начинают порождать странные предупреждения. Ниже — короткое объяснение, почему так происходит, и как задать типы, чтобы инструменты оставались полезными, а не шумными.

Воспроизводим проблему

Функция принимает необязательный булев флаг и произвольное число дополнительных значений типа int. Кажется, что распаковать в неё словарь — просто, но Pylance ругается.

def process(flag_optional: bool = False, **extras: int):
    pass
process(**{"some_input": 5})  # Ошибка Pylance

Если сделать флаг обязательным, всплывает ещё один нюанс. Вызов ниже проходит при настройках по умолчанию, хотя в булев параметр попадает вовсе не bool.

def process_required(flag_required: bool, **extras: int):
    pass
# Допускается при выводе типов по умолчанию, но семантически неверно
process_required(**{"flag_required": 5, "some_input": True})

Почему срабатывает проверка

Суть первого предупреждения — возможное присваивание. Словарь с аннотацией или выведенным типом dict[str, int] может содержать ключ, совпадающий с именованным булевым параметром функции. Раз ключ потенциально есть, типизатор обязан предположить, что вместо bool туда приедет int. То, что параметр необязательный, ничего не меняет: это по‑прежнему именованный аргумент, и распакованная мапа может его заполнить.

Вторая особенность связана с тем, как pyright по умолчанию выводит тип словаря. Литерал вроде {"some_input": True, "flag_required": 2} получает тип dict[str, Unknown]. Unknown ведёт себя как Any и молча пропускает несоответствия, поэтому вызов выше не вызывает ошибки типов. Если включить опцию pyright strictDictionaryInference=true, выведенный тип станет dict[str, bool | int], и тогда появится предупреждение, что в bool может попасть int.

Что с этим делать

Один из вариантов — провести булевый флаг через позиционный‑только параметр и ограничить **kwargs строго типом dict[str, int]. Типы становятся строгими, но интерфейс вызова меняется, что не всегда удобно, если вы осознанно передаёте не‑kwargs аргумент через kwargs. Можно вынуть флаг из словаря и передать его позиционно, а остальные пары переслать дальше, но, скорее всего, придётся делать приведения типов ради спокойствия проверяющего — и это рискует скрыть другие типовые проблемы.

def handle(mode: bool = False, /, **stash: int):
    pass
# Достать из словаря, передать булево позиционно, остальные int — пробросить дальше
# handle(cast(bool, items.pop("mode", False)), **cast(dict[str, int], items))

Более выразительный подход, поддерживаемый pyright, — описать распаковываемый маппинг через TypedDict: закрепить тип «особого» ключа и при этом разрешить дополнительные элементы‑числа. Для этого нужен экспериментальный параметр extra_items.

# 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}  # ОК
process(**args)  # ОК

Функциональность предложена в PEP 728 и, вероятно, появится в Python 3.15. Пока её можно получить через typing_extensions; актуальные версии pyright уже поддерживают её.

Если флаг обязателен, тот же шаблон ловит неверные значения при распаковке:

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}))  # Ошибка типов для flag_required

Почему это важно

Распаковка словаря в аргументы функции — частый мостик между динамическими данными и типизированными API. Если форму маппинга описать неточно, вы получите либо ложные срабатывания, мешающие чистому коду, либо пропуски, скрывающие реальные дефекты. Ужесточение вывода типов словаря или, ещё лучше, TypedDict с extra_items согласует взгляд проверяющего с тем, как функцию предполагается вызывать, и делает диагностику предметной.

Выводы

Если вам важно передавать не‑kwargs параметр через kwargs, позиционные‑только параметры не подойдут. В таком случае отдайте предпочтение TypedDict с extra_items=int: явно задайте особый булев ключ, разрешив дополнительные числовые поля, и включите экспериментальную поддержку в pyright. Если сталкиваетесь с «тихим» принятием некорректных вызовов, включите strictDictionaryInference=true, чтобы словарные литералы со смешанными значениями не деградировали до dict[str, Unknown]. Эти настройки сохраняют гибкость на месте вызова и точность в системе типов без повсюду расставленных # type: ignore.