Noam-N
Noam-N

Reputation: 924

Should a Protocol with @property change runtime behavior in Python?

I thought python Protocol was only useful for type-hints, without any impact on the runtime behavior (except when using @runtime_checkable or default method implementation).

But see this example:

from typing import Protocol

class PortProto(Protocol):
    @property
    def port_id(self) -> str:
        """a read-only port id"""

class MyPortA:
    port_id: str

class MyPortB(PortProto):
    port_id: str


my_port_a = MyPortA()
my_port_a.port_id = "some_id"
print(my_port_a.port_id)  # prints "some_id"

my_port_b = MyPortB()
my_port_b.port_id = "some_id"  # raises "AttributeError: can't set attribute"
print(my_port_b.port_id)

Where line my_port_b.port_id = "some_id" raises AttributeError: can't set attribute.

The only difference between MyPortA and MyPortB is the inheritance of the Protocol.
Is it a bug in Python or the intended behavior?

Yes, I know this line is a violation of the getter-only attribute defined in the Protocol, but this is a type-hint problem for tools like mypy, not something for the runtime.

(Or maybe it's not even a violation of the type-hint, because a read-and-write attribute is a subtype of a read-only attribute).

I expected to see no difference between classes that inherit a Protocol and classes that do not.

Python version: 3.9.7

Upvotes: 2

Views: 127

Answers (1)

Noam-N
Noam-N

Reputation: 924

I think it's related to the following behavior of @property inheritance:

class Port:
    @property
    def port_id(self) -> str:
        """a read-only port id"""

class MyPortB(Port):
    port_id: str

my_port_b = MyPortB()
my_port_b.port_id = "some_id"  # raises "AttributeError: can't set attribute"
from abc import ABC, abstractmethod

class PortBase(ABC):
    @property
    @abstractmethod
    def port_id(self) -> str:
        """a read-only port id"""

class MyPortB(PortBase):
    port_id: str

my_port_b = MyPortB()  # raises "TypeError: Can't instantiate abstract class MyPortB with abstract method port_id"
class MyPortC(PortBase):
    def __init__(self) -> None:
        self.port_id: str = "test"

my_port_c = MyPortC()  # raises "TypeError: Can't instantiate abstract class MyPortC with abstract method port_id"

As you see, the abstract method port_id is considered not implemented in the child, ignoring lines port_id: str and self.port_id: str = "test".

In the following example we see that the property has priority over the class variable:

class MyPortD:
    port_id: str
    private_var: str = "test"

    @property
    def port_id(self) -> str:
        """a read-only port id"""
        return self.private_var

my_port_d = MyPortD()
my_port_d.port_id = "some_id"  # raises "AttributeError: can't set attribute"

So how to implement a read-and-write attribute that will be considered a subtype of a read-only property? The answer is a setter property in the child! see this:

class MyPortG(PortBase):
    def __init__(self) -> None:
        self._port_id: str = "test"

    @property
    def port_id(self) -> str:
        return self._port_id

    @port_id.setter
    def port_id(self, value: str) -> None:
        self._port_id = value

my_port_g = MyPortG()
my_port_g.port_id = "some_id"
print(my_port_g.port_id)  # prints "some_id"

The same principle works for Protocol as well:

class MyPortGUsingProto(PortProto):
    def __init__(self) -> None:
        self._port_id: str = "test"

    @property
    def port_id(self) -> str:
        return self._port_id

    @port_id.setter
    def port_id(self, value: str) -> None:
        self._port_id = value

my_port_g2 = MyPortGUsingProto()
my_port_g2.port_id = "some_id"
print(my_port_g2.port_id)  # prints "some_id"

This solution works, but I still don’t know why a variable is not considered a subtype for a getter-only property when a combination of getter and setter properties does.

See also:
It's possible in C# to have an interface with only a getter property, and the implementation doesn't have to be read-only

Upvotes: 1

Related Questions