pythonpython-typingpartial

How to typehint functools.partial classes


I'm struggling to type hint correctly a python dataclass (or any class) with partial initialization, it seems that the type is lost somewhere but not sure where:

from dataclasses import dataclass
from functools import partial
@dataclass
class Test:
    a : int
    b : int

    @classmethod
    def test_partial[T: Test](cls: type[T], b:int) -> partial[T]: 
        return partial(cls, b=b)

    @classmethod
    def test_partial2(cls, b:int): 
        return partial(cls, b=b)

part = Test.test_partial(3)
part( # linter suggest nothing when passing here

part2 =partial(Test, b=3)
part2( # linter suggest a=...,b=... when passing 

# interestingly enough letting out the typehint, the linter knows again

part = Test.test_partial2(3)
part( # linter suggest a=...,b=... when passing 


The interesting thing is that pyright get's it right when I don't give it to them, so probably there is a way. Any clue?

PD: regarding to the similar question, notice that I'm not asking how to typehint in a mypy compliant maner (no error) but in a maner that tells pyright (or mypy) the arguments that are left.

so solutions such as -> Any: or -> partial[...] may work in the other case but not in this one. and also protocols are not ok as need to be dynamic.


Solution

  • Several issues here:

    Q: How to typehint functools.partial classes?

    You don't, because functools.partial is not expressible with standard type annotations. It is special cased in both mypy and pyright; you can see the special-casing algorithm in their respective repositories, e.g.:

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

    Q: The interesting thing is that both mypy and typeright get's it right when I don't give it to them

    This doesn't sound right for mypy. mypy and pyright act fundamentally different here for your example:

        @classmethod
        def test_partial2(cls, b:int): 
            return partial(cls, b=b)
    

    mypy requires type annotations to be added to the return type to correctly infer the type in the caller scope, while pyright automatically infers the return type if a return type annotation is not provided. If you don't add a return type annotation, like here in test_partial2, mypy infers the return type to be Any.

    Therefore, only pyright is capable of correctly inferring the type in the calling scope of test_partial2, because it doesn't require a type annotation. It's almost* impossible for mypy to correctly infer the type here, because any return type annotation added by the user erases the special casing done by mypy for functools.partial.


    I say almost impossible, because technically it is possible by implementing a decorator factory which takes a functools.partial expression to transform the function defined underneath, but this solution could look much more complicated than it's worth to solve the original problem.