Python Protocol Allowing Optional Attributes

Viewed 587

Suppose I have a protocol Foo.

from typing import Protocol, Optional


class Foo(Protocol):
    val: Optional[int]

And a function like the following:

def func(obj: Foo) -> None:
    if obj.val is not None:
        print(obj.val)

I would expect that either ImplA and ImplB (shown below) should be an acceptable argument for func, since val: int is a more specific than val: Optional[int].

class ImplA:
    val: int


class ImplB:
    val: Optional[int]

a = ImplA()
b = ImplB()

func(a)  # It works
func(b)  # Argument 1 to "func" has incompatible type "ImplB"; expected "Foo"

As it doesn't work, I have to define Bar and take the union of both classes. However, is there a way to construct a protocol such that both val: Optional[int] and val: int will be accepted without Union?

class Bar(Protocol):
    val: int

def func(obj: Union[Foo, Bar]): ...

EDIT:

To achieve this, we need to make sure that the type of val is covariant. As far as I see now, it looks like that Protocol defines it to be an invariant. Is it thus impossible to achieve my requirements?

1 Answers

I ran your code and it raised an error for both func(a) and func(b). The error is related to

if obj.val is not none:

because none of the objects have the attribute "val". I tested the code with Python 3.9.9.

I modified the clause to test if the object has the attribute val, hopefully this helps.

from typing import Protocol, Optional


class Foo(Protocol):
    val: Optional[int]


def func(obj: Foo) -> None:
    if hasattr(obj, 'val'):
        print(obj.val)
    else:
        print(f"{obj} variable val is undefined")


class ImplA:
    val: int


class ImplB:
    val: Optional[int]


print("Accessing func with ImplA and ImplB before initializing val")
a = ImplA()
b = ImplB()
func(a)
func(b)

print("Accessing func with ImplA and ImplB after initializing val")
a = ImplA()
a.val = 2
b = ImplB()
b.val = 20
func(a)
func(b)

Related