2

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

9
  • This isn't a type check. When you define the getter without the setter, you can't assign the attribute at all. This would happen without using Protocol. Commented Jun 4, 2024 at 19:38
  • In 3.11, when I make an ordinary class with a getter and no setter, the error is "AttributeError: property 'a' of 'TestClass' object has no setter" Commented Jun 4, 2024 at 19:39
  • Inheritance actually has a runtime impact in addition to telling the type checker that your class implements the protocol. The runtime doesn't have special treatment for the Protocol type that would avoid that error (nor would it make sense for it to). Commented Jun 4, 2024 at 19:54
  • Does this answer your question? Inherting from Protocols in Python Commented Jun 4, 2024 at 19:58
  • @InSync not it's not Commented Jun 5, 2024 at 10:46

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.