2025, Nov 12 15:02
Ковариантность и контравариантность в Python: случай с Union, который пропускают mypy, Pyright и Pyre
Разбираем, почему ковариантная TypeVar в параметрах метода недопустима даже внутри Union, и mypy и Pyright это пропускают. Советы по безопасным аннотациям.
Ковариантность и контравариантность в системе типов Python на бумаге кажутся простыми, но есть тонкий крайний случай, который проскальзывает мимо популярных статических анализаторов. Параметры методов должны быть контравариантными, то есть ковариантная переменная типа не должна встречаться во входной позиции. Тем не менее mypy, pyright и pyre-check принимают параметр метода, если он — объединение (union), включающее ковариантную переменную типа. Разберёмся, что происходит и как с этим обходиться в реальном коде.
Исходная постановка
Ситуацию легко воспроизвести. Ковариантная TypeVar отклоняется при прямом использовании в качестве типа параметра, но ошибок нет, если та же переменная типа вложена в объединение.
from typing import TypeVar, Generic, Any
U_cov = TypeVar("U_cov", covariant=True)
class Bar(Generic[U_cov]):
def alpha(self, param: U_cov) -> Any: # Должно быть отклонено
...
def beta(self, param: int | U_cov) -> Any: # Принимается популярными проверщиками типов
...
Почему это выглядит неверно
По задумке типы параметров контравариантны. Если переменная типа объявлена ковариантной, её не должно быть во входных позициях, иначе рушатся привычные гарантии подтипирования. При прямой аннотации, как в первом методе, проверщики типов отмечают проблему. Неожиданность в том, что обёртка той же ковариантной переменной в объединение проходит без предупреждений.
Что на самом деле делают проверщики типов
Такое поведение объясняется тем, как текущие инструменты реализуют проверки, а не каким‑то особым послаблением с точки зрения теории типов. В одной реализации логика отклоняет ковариантные переменные типа в параметрах, но при этом явно оставлена задача проверить внутренние переменные типа — то есть случаи с объединениями и другими вложенными формами. Другая реализация изначально блокировала недопустимые вложенные использования, но затем ослабила правило, помечая только прямые ковариантные переменные типа. Как сказано в описании изменения, объединения, содержащие ковариантные переменные типа, были разрешены без дополнительной аргументации.
Проверка для использования ковариантных переменных типа в аннотации входного параметра функции сделана менее строгой. В частности, теперь допускаются объединения, содержащие ковариантные переменные типа.
На практике во всех инструментах результат одинаков: прямое использование отклоняется, вариант с объединением принимается.
Текущее состояние дел
Перед нами незакрытая функциональность. Инструменты сейчас не диагностируют вложенные появления ковариантной переменной типа в типе параметра, а в одном из них правило намеренно ослабили, чтобы разрешить объединения. Официального исключения для такого паттерна нет — его просто не проверяют в этих случаях.
Как поступать в своём коде
Не полагайтесь на подобное «принятие». Если по вашей модельной интуиции аннотация должна быть запрещена, относитесь к случаю с объединением так же. Не размещайте ковариантную переменную типа во входной позиции параметра, даже если она спрятана внутри объединения. Так ваш замысел останется согласован с правилами вариантности вне зависимости от того, что сегодня упускает проверщик.
Почему это важно
Системы типов — это средство коммуникации между людьми и машинами. Когда между задуманным правилом и тем, что реально проверяют инструменты, образуется разрыв, командам легко невольно вшить в API неверные предпосылки. Конструкция может казаться корректной, раз она проходит CI, хотя базовое правило выдало бы предупреждение, загляни проверщик на уровень глубже. Ясность в позициях, чувствительных к вариантности, помогает избегать таких рассинхронов.
Выводы
Ковариантные переменные типа и позиции параметров несовместимы — как при прямом использовании, так и в обёртке объединения. Популярные проверщики типов Python сейчас пропускают такой вариант из‑за неполных или намеренно смягчённых проверок, а не потому, что это особый безопасный случай. Пишите код так, будто действует более строгое правило, и следите за обновлениями выбранного проверщика, когда появится рекурсивная проверка вложенных типов.