pythonpython-typingpython-decorators

Python type hinting None | Object with decorator


Is it possible to add/overwrite a type hint in case of the following example? The example is just to get an idea of what I mean, by no means is this something that I would use in this way.

from dataclasses import dataclass


def wrapper(f):
    def deco(instance):
        if not instance.user:
            instance.user = data(name="test")
        return f(instance)

    return deco


@dataclass
class data:
    name: str


class test_class:
    def __init__(self):
        self.user: None | data = None

    @wrapper
    def test(self):
        print(self.user.name)


x = test_class()
x.test()

The issue is that type hinting does not understand that the decorated method's user attribute is not None, thus showing a linting error that name is not a known member of none.

Of course this code could be altered so that instead of using a decorator it would just do something like this:

def test(self):
   if not self.user:
      ...
   print(self.user.name)

But that is not the point. I just want to know if it is possible to let the type hinter know that the attribute is not None. I could also just suppress the warning but that is not what I am looking for.


Solution

  • I would use the good ol' assert and be done with it:

        ...
        @wrapper
        def test(self):
            assert isinstance(self.user, data)
            print(self.user.name)
    

    I realize this is a crude way as opposed to some annotation magic you might have expected for the decorator, but in my opinion this is the most practical approach.

    There are countless other situations that can be constructed, where the type of some instance attribute may be altered externally. In those cases the use of such a simple assertion is not only for the benefit of the static type checker, but can also save you from shooting yourself in the foot, if you decide to alter that external behavior.

    Alternative - Getter

    Another possibility is to make the user attribute private and add a function (or property) to get it, which ensures that it is not None. Here is a working example:

    from __future__ import annotations
    from collections.abc import Callable
    from dataclasses import dataclass
    from typing import TypeVar
    
    
    T = TypeVar("T")
    
    
    @dataclass
    class Data:
        name: str
    
    
    def wrapper(f: Callable[[TestClass], T]) -> Callable[[TestClass], T]:
        def deco(self: TestClass) -> T:
            try:
                _ = self.user
            except RuntimeError:
                self.user = Data(name="test")
            return f(self)
        return deco
    
    
    class TestClass:
        def __init__(self) -> None:
            self._user: None | Data = None
    
        @property
        def user(self) -> Data:
            if self._user is None:
                raise RuntimeError
            return self._user
    
        @user.setter
        def user(self, data: Data) -> None:
            self._user = data
    
        @wrapper
        def test(self) -> None:
            print(self.user.name)
    
    
    if __name__ == '__main__':
        x = TestClass()
        x.test()
    

    Depending on the use case, this might actually be preferred because otherwise, user being a public attribute, all outside code that wants to use TestClass will face the same problem of never being sure if user is None or not, thus being forced to do the same checks again and again.