pythonpython-typingpython-decorators

Why Python type infer error for decorator


Type infer error for decorator

  1. class with __get__ not properly identified

  2. type with Generic lose Type[]

The following are the comparisons of several scenarios.

from typing import TypeVar, Callable, Generic, Type

GetterReturnType = TypeVar("GetterReturnType")


class ClassPropertyDescriptor(Generic[GetterReturnType]):

    def __init__(self, func: Callable[..., GetterReturnType]):
        if isinstance(func, (classmethod, staticmethod)):
            fget = func
        else:
            fget = classmethod(func)
        self.fget = fget

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        return self.fget.__get__(obj, klass)()

def classproperty(func: Callable[..., GetterReturnType]):
    return ClassPropertyDescriptor(func)

T = TypeVar('T')

class B(Generic[T]):
    pass

class A:
    @classproperty
    def a(self):
        return [123]

    @ClassPropertyDescriptor[list[int]]
    def a2(self):
        return [123]

    @ClassPropertyDescriptor[Type[B]]
    def b(self):
        return B

    @ClassPropertyDescriptor[Type[B[int]]]
    def b2(self):
        return B[int]

    @ClassPropertyDescriptor[B[int]]
    def b3(self):
        return B[int]()
# should be list[int]
x = A.a
x2 = A.a2
y = A.b
# should be Type[B[int]]
y2 = A.b2
y3 = A.b3

Here's a screenshot of PyCharm:


Solution

  • Fix: Make __get__ Fully Typed

    To help the type checker, modify the descriptor like this:

    from typing import Generic, Callable, Type, TypeVar, Any
    
    GetterReturnType = TypeVar("GetterReturnType")
    Owner = TypeVar("Owner")
    
    class ClassPropertyDescriptor(Generic[GetterReturnType]):
        def __init__(self, func: Callable[..., GetterReturnType]):
            if isinstance(func, (classmethod, staticmethod)):
                fget = func
            else:
                fget = classmethod(func)
            self.fget = fget
    
        def __get__(self, obj: Any, klass: type | None = None) -> GetterReturnType:
            if klass is None:
                klass = type(obj)
            return self.fget.__get__(obj, klass)()  # This line's return type is now known
    

    This helps some type checkers (e.g., Pyright) reason about what __get__ returns.

    Tip: Use @overload or reveal_type() (in MyPy) for debugging

    If your IDE/type checker still struggles, you can debug with:

    from typing import reveal_type
    reveal_type(A.a2)   # should show list[int]
    reveal_type(A.b2)   # should show Type[B[int]]