2025, Sep 27 21:00

Typing functools.partial from dataclasses: why return annotations break Pyright and Mypy inference—and how to fix

Learn why annotating functions that return functools.partial from dataclasses breaks Pyright/Mypy inference and autocompletion, and how to fix it quickly

Typing functools.partial with dataclasses: why your annotations break inference and how to fix it

Problem

You build a callable via functools.partial from a dataclass (or any class) and want your type checker to suggest the remaining constructor arguments. As soon as you add explicit type hints to the factory method that returns partial, completions disappear. Drop the hints, and pyright suddenly knows exactly which parameters are left.

Repro

The following example shows a dataclass and two classmethods that create a partially applied constructor. One of them is annotated, the other is not.

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(

In practice, tooling tends to suggest nothing for p( after the annotated version, but suggests x=..., y=... when calling p2( and p3(.

Why this happens

functools.partial is not expressible with standard type annotations. Both mypy and pyright handle it via special-casing, not through normal typing constructs. Their algorithms for this are implemented internally; you can inspect them in their codebases, for example in mypy’s functools handling and pyright’s constructor transform logic: mypy and pyright.

Because of that, the best way to get correct types for a callable produced by functools.partial is to avoid annotating the partial result. Adding an explicit return annotation removes the analyzer’s opportunity to apply its special-case logic to the expression that creates the partial.

There is also a behavioral difference between tools in this specific scenario. pyright can infer the return type of a function that returns partial if you omit the return type annotation, so it provides accurate argument suggestions at the call site. mypy, on the other hand, infers Any for such a function without an explicit return type, which prevents correct propagation of the remaining-arguments signature. Adding a return annotation for mypy does not help either, because that erases the special-casing it would otherwise apply to the partial expression.

To get correct types for a callable built from functools.partial, you must not provide annotations.

This is why the unannotated version behaves better with pyright, while the annotated version loses useful completions.

The fix

Return the partial without a return type annotation so the analyzer can apply its special handling. Keep the program logic the same.

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(

With this shape, pyright can infer the remaining parameters of the partially applied constructor and surface them in autocompletion.

For mypy, this situation is almost impossible to model cleanly: without a return type, it treats the function as returning Any; with a return type, the special-casing no longer applies. Technically, there are advanced workarounds (for example, transforming the function via a decorator factory that takes the partial expression), but they tend to be far more complex than the original problem warrants.

Why it matters

Accurate autocompletion and call-site validation rely on the type checker’s ability to understand how much of a callable’s signature remains after partial application. If you force a return annotation onto a partial-producing function, you can lose that understanding and degrade both developer experience and static checks in editors that depend on the analyzer’s special logic.

Conclusion

If you need correct remaining-argument inference for functools.partial, avoid annotating the return type of functions that produce it. This lets pyright apply its special-casing and provide precise completions. Be aware that mypy behaves differently here; omitting the annotation leads to Any, and adding one erases the special handling. If dynamic behavior is required and protocols are not suitable, keep the factory unannotated and lean on the analyzer’s built-in inference.

The article is based on a question from StackOverflow by Ziur Olpa and an answer by dROOOze.