2

I'm using code such as this to add properties dynamically to a class:

class A:
    def __init__(self, props) -> None:
        for name in props:
            setattr(self.__class__, name, property(lambda _: 1))

This will work as expected:

A(["x"]).x   # outputs 1

However, Pylance (at least in vscode) will (understandably) complain with Cannot access member "x" for type "A" Member "x" is unknown.

Is there a way to help Pylance understand what is going on?

Note: Adding the properties to __annotations__ doesn't help, the warning remains:

class A:
    def __init__(self, props) -> None:
        self.__class__.__annotations__ = {}
        for name in props:
            setattr(self.__class__, name, property(lambda _: 1))
            self.__class__.__annotations__[name] = int

Variant B:

What if the code was less dynamic, like so?

class A:
    PROPS = ["x", "y", "z"]
    def __init__(self) -> None:
        for name in self.PROPS:
            setattr(self.__class__, name, property(lambda _: 1))

Any chance to help Pylance in this scenario?

myke
  • 479
  • 3
  • 14
  • 1
    I guess you still want pylance to complain for `A(["x"]).y`? I fear this isn't possible with static code analysis. The only hope I see is using overloads on Literal values returning A_x and A_y and A_xy types... – Robin Gugel Mar 11 '23 at 09:18
  • @RobinGugel That makes a lot of sense. On second thought I also don't see how this should be possible with static analysis. – myke Mar 11 '23 at 09:23
  • @RobinGugel What about Variant B I just added? Any chance in such a scenario? – myke Mar 11 '23 at 09:29
  • I fear not. Maybe I could help you more if I knew why you needed that, but as far as I know you need to annotate each value explicitly (possibly just in an `if TYPE_CHECKING` block if you need to generate the getters themselfs dynamically) – Robin Gugel Mar 11 '23 at 09:41
  • where do you get the `PROPS` list from? Possibly I could help you using `__getattr__` and literals. – Robin Gugel Mar 11 '23 at 09:47
  • Dynamically created properties will never be statically detectable. Pylance doesn't run your code. It is relying on you to define your properties in the standard way. – ringo Mar 16 '23 at 06:11

2 Answers2

2

In general I would recommend you to avoid creating these dynamic properties and specify them explicitly.

class A:
    @property
    def x(self):
        return 1
    ...

Depending on your use case this might not be possible though. If you know which properties are required but need to create the function body dynamically you can use the TYPE_CHECKING to just define the property headers.

from typing import TYPE_CHECKING
class A:
    PROPS = ["x", "y", "z"]
    def __init__(self) -> None:
        for name in self.PROPS:
            setattr(self.__class__, name, property(lambda _: 1))
    
    if TYPE_CHECKING:
        @property
        def x(self): ...
        
        @property
        def y(self): ...

        ...

In case you have like a huge string list laying around and you want to avoid typing so many def x(self): ... there's also a possibility using __getattr__.

from typing import TYPE_CHECKING
class A:
    PROPS = ["x", "y", "z"]
    def __init__(self) -> None:
        for name in self.PROPS:
            setattr(self.__class__, name, property(lambda _: 1))

    if TYPE_CHECKING:
        def __getattr__(self, _prop: Literal["x", "y", "z"]) -> int: ...

This is possibly the most 'dynamic' you can be. Note: That literal type could also be a generic argument if you need to dynamically create these attributes for your child classes. In that case I'd strongly recommend though to use __init_subclass__ instead of __init__ (__init__ will be called for any instance creation - but you define the properties on the class itself - the code in your __init__ should actually run only once)

Robin Gugel
  • 853
  • 3
  • 8
0

Pylance will not complain if you use getattr(). If you know the type at design time you can even provide a type hint:

a = A(['x'])
one:int = getattr(a, 'x')

This works even if the class comes from a third party.

cdude
  • 141
  • 1
  • 5