2025, Oct 20 03:16

Баг mypy при сложении списков в Python 3.11: как обойти

Баг mypy 1.16 в Python 3.11: конкатенация списков list[str | int] падает при return и присваивании. Причины проблемы и рабочий обход через распаковку [*a, *b].

При работе с Python 3.11 и mypy 1.16 можно столкнуться с неприятным несоответствием: конкатенация списков с совместимыми типами элементов проходит проверку типов как отдельное выражение, но тот же самый результат, помещённый прямо в оператор return, вызывает ошибку. Кажется, что обычное объединение (union) типов элементов должно подходить, однако на границе функции mypy с этим не согласен.

Минимальный симптом

def combine() -> list[str | int]:
    alpha: list[str | int]
    beta: list[int]
    alpha + beta  # Допустимо как выражение
    return alpha + beta  # ошибка: Unsupported operand types for + ("list[str | int]" and "list[int]")  [operator]

Суть путаницы в том, что одна и та же операция трактуется по-разному в зависимости от места. Внутри тела функции — допустимо, в позиции возвращаемого значения — нет.

Что на самом деле происходит

Это поведение — давняя проблема mypy. Её уже не раз фиксировали и поднимали, но она остаётся нерешённой. Существует черновой pull request, который пытается её исправить. Ссылки: отчёт об ошибке на https://github.com/python/mypy/issues/3933 и черновой PR на https://github.com/python/mypy/pull/19399.

Есть и сопутствующее наблюдение: если присвоить результат конкатенации временному имени без аннотации, mypy выводит совместимый тип, и функция завершается без претензий. Но стоит явно аннотировать эту временную переменную конкретным типом вроде list[str | int], как ошибка возникает уже на присваивании.

gamma: list[str | int] = []
delta: list[int] = []
merged = gamma + delta
reveal_type(merged)  # list[int | str]

return merged  # корректно
merged_explicit: list[str | int] = gamma + delta  # ошибка
reveal_type(merged_explicit)  # list[str | int]

return merged_explicit  # корректно

Это подтверждает, что проблема не ограничивается возвращаемыми значениями: она проявляется и при проверке результата конкатенации на соответствие явному целевому типу. Ещё одна связанная тема: https://github.com/python/mypy/issues/3351.

Практический обходной путь

Пока баг не исправлен в самом mypy, рекомендуется заменить сложение списков распаковкой — этот вариант mypy принимает во всех перечисленных сценариях.

def combine_fixed() -> list[str | int]:
    alpha: list[str | int]
    beta: list[int]
    return [*alpha, *beta]  # корректно

Если удобнее работать через временную переменную, приём тот же.

gamma: list[str | int] = []
delta: list[int] = []

epsilon = [*gamma, *delta]
reveal_type(epsilon)  # list[int | str]

return epsilon  # корректно

Почему это важно

Проверяющие типы инструменты влияют на то, как мы строим и осмысливаем код. Такие тонкие несогласованности приводят к ложным срабатываниям, лишним спорам на ревью и ненужным рефакторам. Осознавая текущие ограничения, можно спокойно выпускать код, сохраняя точность аннотаций, и отличать настоящие проблемы типов от особенностей конкретного инструмента.

Выводы

Если сложение списков с элементами объединённого типа проходит как самостоятельное выражение, но падает при возврате или присваивании явно аннотированной цели, вероятно, вы столкнулись с известным багом mypy. Предпочитайте распаковку вида [*a, *b], чтобы обойти проблему, или присваивайте результат неаннотированной временной переменной, полагаясь на вывод типов. Следите за задачей и черновым PR по ссылкам выше: когда исправление попадёт в релиз, исходная запись, возможно, снова станет рабочей без обходов.

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