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.