Search code examples
pythondecoratormypytyping

How do I properly type-hint a decorated __getitem__ and __setitem__


For example:

T = TypeVar("T", bound="CustomDict")

class CustomDict(dict):
    def __init__(self) -> None:
        super().__init__()

    class dict_operator:
        def __init__(self, method: Callable[..., Any]) -> None:
            self.method = method

        def __get__(self, instance: T, owner: Type[T]) -> Callable[..., Any]:
            def wrapper(key: Any, *args: Any, **kwargs: Any) -> Any:
                results = self.method(instance, key, *args, **kwargs)

                print("Did something after __getitem__ or __setitem__")
                return results

            return wrapper

    @dict_operator
    def __getitem__(self, key: Any) -> Any:
        return super().__getitem__(key)

    @dict_operator
    def __setitem__(self, key: Any, value: Any) -> None:
        super().__setitem__(key, value)

Mypy: Signature of "__getitem__" incompatible with supertype "dict"Mypyoverride Signature of "__getitem__" incompatible with supertype "Mapping"Mypyoverride (variable) Any: Any

I take it the decorator has altered the overriden methods' signatures but I don't know how to account for this.


Solution

  • The issue isn't the type annotations on __getitem__ and __setitem__. For better or for worse, mypy (currently) doesn't recognise that a __get__ which returns a Callable is, for the majority of cases, a safe override in lieu of a real Callable. The fastest fix is to add a __call__ to your dict_operator class body (mypy Playground 1):

    class CustomDict(dict):
        ...
        class dict_operator:
            def __init__(self, method: Callable[..., Any]) -> None: ...
            def __get__(self, instance: T, owner: Type[T]) -> Callable[..., Any]: 
            # This needs to be the same as the return type of `__get__`
            __call__: Callable[..., Any]
        
        @dict_operator
        def __getitem__(self, key: Any) -> Any: ...  # OK
    
        @dict_operator
        def __setitem__(self, key: Any, value: Any) -> None: ... # OK
    

    I don't know if you only did this for demonstration purposes, but IMO, you have too much imprecise typing here; Callable[..., Any] in particular is not a very useful type annotation. I don't know what Python version you're using, but if you're able to use typing_extensions instead, you can have access to the latest typing constructs for better static checking. See mypy Playground 2 for a possible way to implement stricter typing.