2025, Oct 01 05:16

issubclass и Any в mypy: почему тип не сужается и что делать

Почему issubclass не сужает Any в mypy и к чему это приводит. Покажем шаблоны: проверка isinstance(x, type), явные аннотации type и кейсы.

Почему простой вызов issubclass() может незаметно не сузить типы в mypy — и что с этим делать

Обзор проблемы

Сужение типов в mypy обычно кажется очевидным — пока в кадре не появляется Any. Распространённая ловушка — рассчитывать, что issubclass() уточнит значение типа Any до более конкретного. На деле этого не происходит: в ветке, где проверка issubclass() выполняется напрямую, никакого полезного сужения не получается.

Минимальный пример

Отрывок ниже показывает расхождение ожиданий и реальности. В первой ветке мы совмещаем isinstance(x, type) с issubclass(), и mypy корректно сужает тип. Во второй — вызываем только issubclass(), и тип остаётся Any.

from typing import Any

class BaseUnit:
    pass

def probe(x: Any) -> None:
    if isinstance(x, type) and issubclass(x, BaseUnit):
        reveal_type(x)  # Выявленный тип: "Type[BaseUnit]"

    if issubclass(x, BaseUnit):
        reveal_type(x)  # Выявленный тип: "Any"

Что происходит

В mypy есть известная проблема: issubclass() не выполняет сужение, если проверяемое значение имеет тип Any. Даже если сама проверка логически проходит, для анализатора тип по-прежнему остаётся Any, поэтому во второй ветке примера сужения не происходит.

Есть и другой аспект второй проверки: первый аргумент issubclass() должен быть типом. Если это не так, вызов может закончиться исключением во время выполнения. Иными словами, issubclass(x, BaseUnit) фактически выступает как неявное утверждение, что x — это тип, что порой нежелательно. Если такое утверждение действительно подразумевается, лучше явно отразить это в аннотации переменной, например указав x: type.

Последствия, в теории

Ниже — иллюстрация побочного эффекта этого «утверждения» с точки зрения системы типов. Это теоретическая картинка: ни один популярный проверяющий типов сейчас не ведёт себя ровно так, но пример подчёркивает, что вызов несёт неявное предположение о том, что первый аргумент — тип.

# Теоретический вывод. Ни один из основных проверяющих типов сейчас так не работает.
def inspect(y: Any):
    if issubclass(y, int):
        reveal_type(y)  # тип[int]

    reveal_type(y)      # тип       (!?)

Как подойти к исправлению

Отсюда два практических вывода. Во‑первых, при работе с Any не рассчитывайте на сужение через issubclass(): это известное ограничение. Во‑вторых, если код вызывает issubclass(), убедитесь, что первый аргумент действительно является типом. Самый безопасный путь — поставить перед вызовом защиту isinstance(x, type) или, если такое утверждение задумано, явно аннотировать переменную как type.

Исправленный пример

Первый вариант явно фиксирует требование на этапе выполнения и сохраняет сужение в форме, понятной mypy.

from typing import Any

class BaseUnit:
    pass

def verify(a: Any) -> None:
    if isinstance(a, type) and issubclass(a, BaseUnit):
        reveal_type(a)  # Выявленный тип: "Type[BaseUnit]"

Если функция изначально должна работать только с типами, закрепите этот контракт заранее. Это предотвращает случайное неправильное использование и проясняет намерение.

class BaseUnit:
    pass

def ensure(b: type) -> None:
    if issubclass(b, BaseUnit):
        pass  # b — это тип по аннотации

Зачем это важно

Опора на issubclass() для сужения значений типа Any создаёт иллюзию надёжности. Проверка может пройти во время выполнения, но типовая информация останется расплывчатой, что лишает статический анализ смысла и может скрыть ошибки. К тому же передача не‑типа в issubclass() приводит к исключению, поэтому воспринимать этот вызов как де‑факто утверждение, не закреплённое аннотациями, — риск сделать код хрупким.

Выводы

Если значение имеет тип Any, не ожидайте сужения через issubclass(): это известное ограничение mypy. При вызове issubclass() гарантируйте, что первый аргумент — это тип: добавьте защиту isinstance(x, type) или аннотируйте переменную как type, если именно такой контракт и задуман. Эти небольшие шаги синхронизируют поведение проверяющего типов и рантайма, уменьшают сюрпризы и делают кодовую базу проще для понимания.

Статья основана на вопросе на StackOverflow от Leonardus Chen и ответе от InSync.