Maybe it should be a type checker error?
The section in the PEP on attributes gives some examples that expect Self to specialize to subclasses.
@dataclass
class LinkedList(Generic[T]):
value: T
next: Self | None = None
@dataclass
class OrdinalLinkedList(LinkedList[int]):
def ordinal_value(self) -> str:
return as_ordinal(self.value)
xs = OrdinalLinkedList(value=1, next=LinkedList[int](value=2))
# Type error: Expected OrdinalLinkedList, got LinkedList[int].
I donât think itâs particularly useful to always treat Self as syntactic sugar for the enclosing class when used in attributes. I worry that this would be too restrictive in practice, although a better argument would have examples from real world code. Maybe you have a better sense of the real world usage of Self in attributes?
I believe it would be sound to treat Self in attributes as specialized to subclasses, but if we distinguished between the different instantiations of Self that might appear in methods. For example:
class C:
x: Self
def __init__(self):
self.x = self # OK
def foo(self, other: Self):
self.x = other # Error
def bar(self):
self.x = self # OK
I think itâs confusing that Self is not compatible with Self, but I think it would work.
The problem I see with the class type var interpretation is as follows:
a = A() # a is an A[Self = A[...]] = A* (I just made up notation)
b = B() # b is a B[Self = B[...]] = B*
# but Self is an invariant type parameter so although B[T] is a subclass of A[T], B* is not a subclass of A*
Note that this is only an issue when Self appears in a contravariant position. If it is only present in covariant locations then B* is a subclass of A*.
Looking at it, I donât actually think this is a bad interpretation though. This interpretation allows you to have Protocols for things like strictly typed binary operators
. It does explain why unsound behavior appears. If you assume A* == A and B* == B then you make the incorrect assumption that B* is a subclass of A* if B is a subclass of A.
This is a very non-intuitive interpretation if you try to use Self in a contravariant location in a base class and your base class is not a protocol or an abstract class, as it means you have to reject o: A = B()
Perhaps not. It is nice to have a spelling that doesnât require forward-references (quoted annotations etc), though in 3.14+ with PEP 649 this will become less relevant.
I donât think I do, would need to do a study.
I donât think this change would address the unsoundness of attribute Self. Consider:
from typing import Self
class A:
x: Self
class B(A):
pass
def set_x(a: A):
a.x = A()
b = B()
set_x(b)
# if we reveal B here, we are unsound; it is A at runtime
reveal_type(b.x)
Attribute Self could soundly specialize in subclasses only if it were used in a ReadOnly attribute (if we have PEP 767).
Do you mean that any use of Self to type an attribute should be a type checker error?
Iâm not sure. After reading some of your comments, I looked at uses of Self attributes, and only found what looked like âsyntactic sugar for the name of the enclosing classâ. Your set_a snippet is another example of that. Therefore, in my opinion, it would be better to just force people to write the class name if thatâs what they mean. Writing Self risks readers of code thinking that subclasses see a narrower constraint.
I agree with you 100% about ReadOnly[Self] would ideally be specialized for subclasses.
Historically soundness for attribute has not really been paid much attention to when it comes to typing enforcement. The example Carl gave earlier on unsound Self attribute shares similar underlying issue with examples that any mutable attribute whose type is a covariant type variable is unsound, i.e.
_Self = TypeVar("_Self", covariant=True)
class Placeholder(Generic[_Self]):
x: _Self
class A: pass
class B(A): pass
def set_x(a: Placeholder[A]) -> None:
a.x = A()
b = Placeholder[B]()
set_x(b)
# if we reveal B here, we are unsound; it is A at runtime
reveal_type(b.x)
Pre-existing type checkers alway enforce that âcovariant type variable cannot appear at contravariant position, and contravariant type variable cannot appear at covariant positionâ for types that appear on method annotations. But the problem here is that the enforcement is skipped for attributes, where mutable attribute annotation should be treated as invariant position and read-only attribute annotation should be treated as covariant position.
If we grant the intuition that Self type is a special kind of covariant type variables, then Iâd prefer a world where type variable variance are treated consistently across the board, e.g.
- If we want to intentionally keep it such that attribute annotations should not be enforced for variance (for backward-compatible reasons), then I feel it might be easier if we donât constrain ourselves to look for solutions that has to be sound with respect to
Selfattributes. - If we consider the soundness of
Selfattribute treatment as important, then I feel weâd want to eventually tighten up the enforcement for mutable attributes with covariant type variable as well.
Of course my earlier points are kind of tangential to the main discussion, which is how to interpreter the Self annotation on methodsâŚ
If we donât want to take the position to ban all contravariant Self occurrences, then intuitively I think that Samâs interpretation (Self bound to the class not the method) is more useful (something else needs to be done to address the soundness issue though), given that the contravariant Self examples Iâve seen are mostly about strongly-typed binary operators, and my understanding is that Carlâs interpretation would not allow subtype operator implementation to ârestrictâ the type of the other operand. That said I donât think Iâve seen enough code to claim that strongly-typed binary operator would be the main use case of contravariant Self.
No, because this is precisely the thing that is unsound to do (it breaks LSP). I donât think there is any way to âaddress the soundness issueâ while allowing this.