2025, Sep 27 21:17

Типизация functools.partial с датаклассами: почему аннотации ломают вывод типов и как это исправить

Разбираем, почему аннотации типов ломают вывод параметров у functools.partial в датаклассах: поведение pyright и mypy и как вернуть подсказки аргументов.

Типизация functools.partial с датаклассами: почему аннотации ломают вывод типов и как это исправить

Проблема

Вы создаёте вызываемый объект через functools.partial из датакласса (или любого класса) и хотите, чтобы проверка типов подсказывала оставшиеся аргументы конструктора. Как только вы добавляете явные аннотации типов в фабричный метод, который возвращает partial, подсказки исчезают. Уберите аннотации — и pyright тут же понимает, какие параметры ещё не переданы.

Пример

В следующем примере показан датакласс и два classmethod, создающих частично применённый конструктор. Один метод аннотирован, другой — нет.

from dataclasses import dataclass
from functools import partial

@dataclass
class Sample:
    x: int
    y: int

    @classmethod
    def make_part_typed[U: Sample](cls: type[U], y: int) -> partial[U]:
        return partial(cls, y=y)

    @classmethod
    def make_part(cls, y: int):
        return partial(cls, y=y)

p = Sample.make_part_typed(3)
p(

p2 = partial(Sample, y=3)
p2(

p3 = Sample.make_part(3)
p3(

На практике инструменты зачастую ничего не предлагают для p( в аннотированном варианте, но подсказывают x=..., y=... при вызове p2( и p3(.

Почему так происходит

functools.partial нельзя выразить стандартными аннотациями типов. И mypy, и pyright обрабатывают его через специальные правила, а не обычные конструкции typing. Их алгоритмы реализованы внутри анализаторов; их можно изучить в коде, например, в обработке functools у mypy и в логике преобразования конструкторов у pyright: mypy и pyright.

Поэтому лучший способ получить корректные типы для вызываемого объекта, созданного через functools.partial, — не аннотировать результат partial. Явная аннотация возвращаемого типа лишает анализатор возможности применить свою специальную логику к выражению, создающему partial.

Есть и различия в поведении инструментов в этом сценарии. pyright способен вывести возвращаемый тип функции, которая возвращает partial, если опустить аннотацию возвращаемого типа, поэтому на месте вызова он даёт точные подсказки по аргументам. mypy же в таком случае выводит Any для функции без явного возвращаемого типа, из‑за чего корректная передача сигнатуры оставшихся аргументов невозможна. Добавление аннотации возвращаемого типа для mypy тоже не помогает, потому что тогда исчезает специальная обработка самого выражения partial.

Чтобы получить корректные типы для вызываемого объекта, построенного с помощью functools.partial, не указывайте аннотации.

Поэтому неаннотированный вариант работает лучше с pyright, а аннотированный теряет полезные подсказки.

Решение

Возвращайте partial без аннотации возвращаемого типа — тогда анализатор сможет применить свою специальную обработку. Логику программы менять не нужно.

from dataclasses import dataclass
from functools import partial

@dataclass
class Sample:
    x: int
    y: int

    @classmethod
    def make_part(cls, y: int):
        return partial(cls, y=y)

p = Sample.make_part(3)
p(

В таком виде pyright может вывести оставшиеся параметры частично применённого конструктора и показать их в автодополнении.

Для mypy эту ситуацию почти невозможно аккуратно смоделировать: без возвращаемого типа функция рассматривается как возвращающая Any; с аннотацией — специальная обработка больше не применяется. Технически существуют продвинутые обходные пути (например, преобразование функции через фабрику-декоратор, принимающую выражение partial), но они обычно сложнее, чем это оправдано изначальной задачей.

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

Точное автодополнение и проверка вызовов зависят от того, способен ли анализатор типов понять, какая часть сигнатуры остаётся после частичного применения. Если вы принудительно аннотируете возвращаемый тип функции, создающей partial, это знание теряется, и страдают и подсказки, и статические проверки в редакторах, полагающихся на специальную логику анализатора.

Вывод

Если вам нужна корректная инференция оставшихся аргументов для functools.partial, не аннотируйте возвращаемый тип функций, которые его возвращают. Так pyright сможет применить свою специальную обработку и дать точные подсказки. Помните, что mypy ведёт себя иначе: без аннотации он получает Any, а при добавлении аннотации специальная обработка исчезает. Если требуется динамика и протоколы не подходят, оставьте фабричный метод без аннотаций и положитесь на встроенный вывод типов анализатора.

Статья основана на вопросе на StackOverflow от Ziur Olpa и ответе от dROOOze.