PEP 767: Annotating Read-Only Attributes

I’m working on implementing experimental support for PEP 767 in pyright. I found a number of issues in the PEP that require additional discussion.

  • The PEP indicates that a read-only attribute can be overridden by a property or other descriptor object. I don’t think that’s sound because of the covariance of type. Consider the following code. Regardless of whether HasName.name is ReadOnly, it is unsound to override it with a descriptor.
from typing import Protocol

class HasName(Protocol):
    name: str

def func(h: type[HasName]):
    reveal_type(h.name)  # Both pyright and mypy reveal str here
    print(h.name.upper())

class NamedProp:
    @property
    def name(self) -> str: ...

# The following call will crash. This should be a type violation,
# and currently mypy and pyright generate an error here. Making
# `name` ReadOnly should not change this.
func(NamedProp)

This affects at least three parts of the spec:

  1. In the “Rationale” section, the PEP says that “A subclass of Member can redefine .id as a writable attribute or a descriptor”. I think you need to delete “or a descriptor”.
  2. The “Subtyping” section says “Read-only attributes can be redeclared as writable attributes, descriptors or class variables”. For the same reason here, “descriptors” cannot be allowed.
  3. In the “Subtyping” section, one of the examples (NamedProp) should be deleted because this is unsound. Similarly, NamedDescriptor should be deleted.
  • The PEP is not clear on whether a “bare” ReadOnly annotation is allowed or should be considered an error. One could argue that this should be an error condition since ReadOnly is a type qualifier, so it should have a corresponding type to qualify. However, one could also make the argument that an implicit type of Any could be assumed if a type annotation is omitted. One could also make the argument that if a bare ReadOnly is coupled with an assignment, inference should be used (as it is with the Final qualifier); however, inference rules would get tricky to define here because (unlike Final), it’s inadvisable to infer literal values because this could cause problems for overrides within subclasses. In any case, additional clarity is needed here.

  • The “Initialization” section includes a comment “cannot assign to a read-only attribute of a base class” even though the code is located within an __init__ method. This seems inconsistent with the fact that a subclass can override a read-only attribute. If it’s able to override, why can’t it reassign a new value in the __init__ method (or __new__ or __init_subclass__) without generating an error?

  • There is an “Open Issues” section that still needs to be resolved. My take on the open issue is that third-party initialization hooks should be out of scope. The initialization rules in this PEP are already pretty onerous for type checkers to implement. Extending these rules to arbitrary third-party initialization mechanisms is not feasible IMO.