pythonpython-typingpyright

Type-hinting a dynamic, asymmetric class property


I'm currently working on removing all the type errors from my Python project in VS Code.

Assume you have a Python class that has an asymmetric property. It takes any kind of iterable and converts it into a custom list subclass with additional methods.

class ObservableList(list):
    """list with events for change, insert, remove, ..."""
    # ...

class MyFrame:
    @property
    def my_list(self) -> ObservableList:
        return self._my_list
    @my_list.setter
    def my_list(self, val: typing.Iterable):
        self._my_list = ObservableList(val)
    # ...

my_frame = MyFrame()

VS Code (i.e. Pyright) will correctly deduce that:

Now, let's assume that there is no actual @property code. Instead, the property is implemented dynamically using __setattr__ and __getattr__. (Context: We're talking about a GUI generator which provides automatic bindings.)

I want to use a declaration on class level to tell the typechecker that this property exists, without actually spelling it out:

class MyFrame(AutoFrame):
    my_list: ???
    # ...

(AutoFrame provides the __getattr__/ __setattr__ implementation.) What can I put in place of the ??? to make this work?

Re: close vote: the linked question's answer basically boils down to going back to square one (implementing the property explicitly). The point of using AutoFrame is specifically to get rid of that repetitive boilerplate code. Just imagine doing this for a GUI frame with a dozen bound controls. I can live with a single added declaration line but not much more.


Solution

  • You can use the Any-Trick, see my answer on how the four different cases work, but in short, if you type it as:

    from typing import Any, reveal_type
    
    class ObservableList(list):
        """list with events for change, insert, remove, ..."""
        
        def foo(self) -> str:
            ...
    
    class MyFrame:
        my_list: ObservableList | Any
    
    MyFrame().my_list = [2, 3] # OK
    
    value: str = MyFrame().my_list.foo()  # OK, add :str to avoid value being str | Any
    

    Of course that will have the downside that any assignment, will be okay as well: MyFrame().my_list = "no error" so you need to be aware of that.


    Alternatively, you can implement the property behind a if TYPE_CHECKING block OR in a .pyi file: no-runtime influence, correct-typing, but boilerplate:

    class MyFrame:
      if TYPE_CHECKING:
        @property
        def my_list(self) -> ObservableList: ...
        @my_list.setter
        def my_list(self, val: typing.Iterable): ...