2025, Nov 25 12:02
Type[Any] в аннотациях Python: почему это «любой класс», а не класс Any
Как проверяющие типы Python трактуют Any: почему type[Any] — это «любой класс», а не класс Any, и какие риски это несёт для API; позиция Pyright/Pylance.
Когда вы пытаетесь принять ограниченный набор классов плюс сам класс Any в сигнатуре функции, легко угодить в ловушку: проверяющие типы трактуют вашу аннотацию совсем не так, как вы задумывали. Частая интуитивная попытка — записать объединение из конкретных объектов классов и type[Any], а параметр по умолчанию выставить в Any. На первый взгляд всё выглядит правильно, но это меняет смысл вашего API так, что имеет значение для статического анализа.
Постановка задачи
Рассмотрим функцию, которая должна принимать объект класса int, объект класса str или сам класс Any, причём по умолчанию — Any:
def run_op(kind: type[int] | type[str] | type[Any] = Any) -> Any:
...
Кажется, что это точный контракт, но это не так. Использование Any внутри type[...] заставляет аннотацию вести себя как «разрешён любой объект класса», а не «разрешён объект класса Any». В итоге происходит сразу две вещи: вы фактически расширяете параметр до любого возможного класса, и строгий проверяющий, такой как Pylance, может пометить это ошибкой вроде "Type \"type[typing.Any]\" is not assignable to declared type \"builtins.type[Any]\"".
Почему так происходит
Во время проверки типов Any не рассматривается как обычный класс. Это специальная форма, которую проверяющие распознают и обрабатывают отдельной логикой. Именно поэтому type[Any] означает примерно «объект класса любого типа», а не «буквальный объект класса Any». Так что объединение в примере уже не обеспечивает задуманное ограничение.
В рантайме Any, напротив, реализован как класс, и в typeshed он тоже описан как класс. От него даже можно наследоваться. Но это поведение на исполнении не меняет того, что при интерпретации подсказок типов проверяющие рассматривают Any как уникальную, непрозрачную конструкцию.
Что не сработает, если пойти другим путём
Можно попытаться опереться на детали реализации времени выполнения и нацелиться на метакласс, используемый «под капотом». Например:
def run_alt(kind: type[int] | type[str] | _AnyMeta = Any) -> Any:
...
На практике этот подход не выдерживает. Нельзя полагаться на импорт или использование _AnyMeta из typing для аннотаций, и даже если бы это удалось, пользы не было бы: проверяющие не рассматривают Any как экземпляр какой‑то иерархии классов — для них это специальная форма без дополнительных свойств, которые можно было бы задействовать через типизацию метакласса.
Время выполнения против проверки типов: короткая демонстрация
Разрыв между поведением в рантайме и семантикой проверяющих хорошо виден, если унаследоваться от Any. На исполнении это допустимо, а проверяющие учитывают это, трактуя такие подклассы как «по сути Any» при доступе к атрибутам:
class X: ...
class Y(Any): ...
x = X()
y = Y()
x.token = 1 # строгие проверяющие сообщат об ошибке: экземпляры X не объявляют 'token'
y.token = 1 # допускается: экземпляры Y рассматриваются как Any
Ещё один показательный пример — присваивание Any переменной, аннотированной как type[Any]. Некоторые проверяющие это принимают, Pyright/Pylance — нет:
klass_ref: type[Any] = Any # Pyright/Pylance: ошибка
# Другие проверяющие могут это принять
Разработчики Pyright прямо фиксируют свою позицию:
[...] Any нельзя присваивать type. Any — это специальная форма. Подробности её реализации не задокументированы и не определены в typeshed. Поэтому проверяющие типы не должны делать предположений о её реализации во время выполнения.
А как насчёт TypeForm?
PEP 747 вводит TypeForm, и может показаться, что это способ выразить «сам тип Any». Однако PEP не определяет способа указать тип самого Any. Более того, TypeForm[Any] — это не «только Any». Он описывает TypeForm, чей типовой аргумент статически не известен, но является валидным объектом формы типа и потому может быть присвоен любому другому TypeForm и принимать присваивания от него.
Какой практический вывод?
Если вам нужен параметр, который принимает конкретные объекты классов плюс сам объект класса Any, сегодня не существует аннотации, одинаково понимаемой всеми проверяющими и надёжно выражающей это намерение. Использование type[Any] неизбежно открывает дверь для любого объекта класса, что разрушает ограничение. Нацеливание на _AnyMeta не помогает, потому что Any обрабатывается как специальная форма, а не как экземпляр метакласса в терминах статической типизации.
Почему это важно
Расхождение между поведением на исполнении и семантикой проверяющих легко упустить из виду, тем более что Any можно наследовать в рантайме и он определён как класс в typeshed. Если опираться на детали рантайма при написании подсказок типов, разные проверяющие будут расходиться во мнениях, и в итоге аннотации либо не закрепят ожидаемый контракт, либо приведут к ложным ошибкам в отдельных инструментах. Понимание того, что Any — особая форма на этапе проверки типов, помогает избегать хрупких и вводящих в заблуждение аннотаций.
Вывод
Не пытайтесь моделировать сам класс Any как часть объединения объектов классов в типе параметра функции. В мире статической типизации Any — это специальная форма, а не обычный класс, и type[Any] означает «любой объект класса», а не «класс Any». Инструменты вроде Pyright/Pylance отвергнут конструкции наподобие присваивания Any переменной типа type[Any], а импорт внутренних деталей вроде _AnyMeta не изменит того, как проверяющие интерпретируют аннотацию. Если дизайн вашего API завязан на различении объекта класса Any и остальных объектов классов, важно понимать: текущая семантика typing не предоставляет переносимого и точного способа выразить это различие.