2026, Jan 04 00:01
Сужение Iterable[str] в строгом режиме Pyright: рабочий type guard
Почему в строгом режиме Pyright all(isinstance) не сужает Iterable[str], и как исправить: TypeIs и type guard для корректной реализации __contains__ в коллекциях
Работа с Pyright в строгом режиме часто выявляет разрыв между проверками во время выполнения и тем, что статический анализатор способен доказать. Типичный случай: вы хотите убедиться, что значение с аннотацией object на деле является Iterable[str], и рассчитываете, что проверяющий типы это поймёт после защитной проверки isinstance. Этого не происходит. Ниже — почему так и как аккуратно это исправить.
Проблема
Вам нужно реализовать __contains__ в подклассе MutableSequence, где сигнатура метода жёстко задаётся как value: object. Во время выполнения вы хотите принимать любой Iterable[str], нормализовать его и сравнить со своими внутренними данными. Прямолинейный вариант выглядит так:
from typing import Iterable
class Holder:
def __init__(self, payload: Iterable[str]) -> None:
self.payload = payload
def __contains__(self, probe: object) -> bool:
if isinstance(probe, Iterable) and all(isinstance(tok, str) for tok in probe):
merged = "".join(probe).upper()
return merged in "".join(self.payload)
return False
Во время выполнения это работает. Но в строгом режиме Pyright переменная внутри генератора, tok, остаётся типа Unknown, и проверяющий не сужает probe до Iterable[str] внутри блока if. Ручной обход, for tok in probe, тоже не помогает: тип элемента по-прежнему неизвестен.
Почему проверка isinstance + all(...) не сужает тип
Pyright не рассматривает all(isinstance(...)) как защиту типов (type guard). В отличие от некоторых встроенных функций вроде filter, для которых можно описать поведение через заглушки, all потребовала бы особой логики в проверяющем и предположений о семантике итерируемого объекта. Мейнтейнеры резюмируют это так:
Проверяющему пришлось бы не только «зашить» знание о функции all, но и делать предположения о семантике итерируемого выражения, с которым она работает.
Поддержку пришлось бы добавлять специально для выражений вида all(isinstance(a, b) for a in [x, y]). Такая форма используется редко, поэтому добавлять для неё отдельную логику нецелесообразно.
Короче говоря, all(...) не сузит probe до Iterable[str] в Pyright, а переменная элемента tok в строгом режиме останется Unknown.
Решение: отдельная защита типов
Рекомендуемый подход — добавить небольшой, переиспользуемый type guard, который явно говорит: «это Iterable[T] с элементами указанного типа». Затем защищаем им тело __contains__. Так сигнатура с value: object остаётся неизменной, а Pyright получает понятный ему сигнал.
from typing import Iterable, TypeIs
# Переиспользуемый гард: проверяет итерируемость и тип элементов
def ensure_iterable_of[T](thing, kind: type[T] = object) -> TypeIs[Iterable[T]]:
return isinstance(thing, Iterable) and all(isinstance(elem, kind) for elem in thing)
class Holder:
def __init__(self, payload: Iterable[str]) -> None:
self.payload = payload
def __contains__(self, probe: object) -> bool:
if ensure_iterable_of(probe, str):
normalized = "".join(probe).upper()
return normalized in "".join(self.payload)
return False
Этот приём сужает probe до Iterable[str] внутри блока if. Если не хочется добавлять помощник, можно сделать cast после проверки. Если строгий режим всё ещё помечает переменную генератора как неизвестную в вызове all(...), можно либо точечно подавить предупреждение reportUnknownArgumentType, либо разбить гард на два шага так, чтобы сама итерация шла по Iterable[Any]; это убирает жалобу на неизвестный аргумент.
from typing import Any, Iterable, TypeIs
# Шаг 1: распознать «итерируемое из любых элементов»
def seems_iterable(source: object) -> TypeIs[Iterable[Any]]:
return isinstance(source, Iterable)
# Шаг 2: уточнить до конкретного типа элементов, опираясь на первый гард
def ensure_iterable_of_type[T](source: object, kind: type[T] = object) -> TypeIs[Iterable[T]]:
return seems_iterable(source) and all(isinstance(unit, kind) for unit in source)
Замечание о поведении во время выполнения
Помимо типизации, помните, что некоторые итерируемые объекты — генераторы или файловые дескрипторы — одноразовые. Проверка all(isinstance(...)) их «съедает», поэтому последующий join увидит пустой поток. Это оговорка уровня исполнения, не связанная с типами, но важная, если вы проверяете и затем повторно итерируете один и тот же объект.
Зачем это важно
Сужение типов в Python держится на шаблонах, которые проверяющий умеет распознавать. Простые защиты вроде isinstance(var, SomeType) понятны; генераторные выражения внутри all(...) — нет. Ожидание, что all(...) уточнит типы, приводит к хрупким предположениям и утечке Unknown в код. Небольшой явный гард на базе TypeIs чётко выражает намерение, точно сужает там, где нужно, и делает строгий режим полезным, а не шумным.
Итоги
Не рассчитывайте на all(isinstance(...)) для уточнения типов элементов Iterable в строгом режиме Pyright. Для сужения это не поддерживается, а «ручная» итерация ничего не меняет. Добавьте простой гард, утверждающий «итерируемое из T», или сделайте приведение типов после ваших рантайм‑проверок — если так удобнее. Следите за одноразовыми итерируемыми: если вы и проверяете, и потребляете их, это важно. С этими поправками вы сохраните требуемый интерфейс __contains__ с параметром типа object и при этом получите чистые, точные типы внутри метода.