# Unsoundness of contravariant \`Self\` type

**URL:** <https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338>\
**Category:** Typing\
**Tags:** typing, typing-spec\
**Created:** [March 28, 2025, 10:30pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338 "2025-03-28T22:30:54Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![grievejia](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/grievejia/32/18850_2.png) [@grievejia](https://discuss.python.org/u/grievejia)\
**Post date:** [March 28, 2025, 10:30pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/1 "2025-03-28T22:30:54Z")

</div>

Consider the following code snippet:

```python
class Base:
    def foo(self, x: "Base") -> None:
         pass

class Derived(Base):
    def foo(self, x: "Derived") -> None:
        x.bar()

    def bar(self) -> None:
        pass

def test(a: Base, b: Base) -> None:
    a.foo(b)
test(Derived(), Base())

```

It’s easy to see that this program is not type-safe: the `foo()` method on the `Base` class is declared to be able to consume _any_ subclasses of `Base` as argument, but the overriding `foo()` method on the `Derived` class can only consume one specific subclass of `Base` (namely `Derived`). This breaks [Liskov substitution principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle), and if we run the program it’ll result in a runtime crash because in the run, `Derived.foo()` would unexpectedly get a `Base` object as argument via dynamic dispatching, while it only expected a `Derived` object. Type checkers today are in universal agreement that this kind of code is problematic and will reject it typically via some kind of “incompatible override” check ([mypy](https://mypy-play.net/?mypy=latest&python=3.12&gist=80607ce2c23dc28103ef963469011acd), [pyright](https://pyright-play.net/?pyrightVersion=1.1.374&code=MYGwhgzhAEBCkFMBcBYAUNT0AmCBm0eA9kQBQQIh4A00AHktAETwVMCU0AtAHzQByRAHbJ0WcZgAOkCOnSgZ0ACIIATgEsAbgmylWCdqgxZcBYmQpVaDZio3bsHbn0EijErHQB0AIzCrSdjljTFNoPwDLPE5eAWFREIlpKGCwgBcECDTSMEZ9Wh88xBiXePcsMC9zUh8gtAys0jstHUDafUD2IA), [pyre1](https://pyre-check.org/play?input=class%20Base%3A%0A%20%20%20%20def%20foo(self%2C%20x%3A%20%22Base%22)%20-%3E%20None%3A%0A%20%20%20%20%20%20%20%20%20pass%0A%0Aclass%20Derived(Base)%3A%0A%20%20%20%20def%20foo(self%2C%20x%3A%20%22Derived%22)%20-%3E%20None%3A%0A%20%20%20%20%20%20%20%20x.bar()%0A%0A%20%20%20%20def%20bar(self)%20-%3E%20None%3A%0A%20%20%20%20%20%20%20%20pass%0A%0Adef%20test(a%3A%20Base%2C%20b%3A%20Base)%20-%3E%20None%3A%0A%20%20%20%20a.foo(b)%0Atest(Derived()%2C%20Base()))).

* * *

But what happens if I change 2 type annotations in this example, and keep everything else (including the program logic) the same as follows?

```python
from typing import Self
class Base:
    def foo(self, x: Self) -> None:
         pass

class Derived(Base):
    def foo(self, x: Self) -> None:
        x.bar()

    def bar(self) -> None:
        pass

def test(a: Base, b: Base) -> None:
    a.foo(b)
test(Derived(), Base())

```

This change should not affect whether the program is type-safe or not – it’s exactly the same code that breaks substitution principle before, and exactly the same crash we’ll run into at runtime as before. So I would expect that this version of the program will be rejected by type checkers as well. However that’s not the case: [mypy](https://mypy-play.net/?mypy=latest&python=3.12&gist=8d12c74bd1edfd694bde36c54c03e142&flags=strict), [pyright](https://pyright-play.net/?pyrightVersion=1.1.374&code=GYJw9gtgBALgngBwJYDsDmUkQWEMoDKApgDbACwAUFQMYkCGAzo1AEJNEBcVUvUAJkWBRgYMAApGpYABooAD06FpASigBaAHxQAcmBRcefY1ARNGVWg2ZQAIkRBIAbkX7j2Uld0rHBw0RJSZHKKymRqWrr6hj4mvPIAdABG9CDiKpaxvH5QKWlBwBHaegbecbxmzJk5MESMMOL0Sh5EcknNHEVRpUa89AkB4kkZlLX14vaOLm4qci3pKkA) and [pyre1](https://pyre-check.org/play?input=from%20typing_extensions%20import%20Self%0A%0Aclass%20Base%3A%0A%20%20%20%20def%20foo(self%2C%20x%3A%20Self)%20-%3E%20None%3A%0A%20%20%20%20%20%20%20%20%20pass%0A%0Aclass%20Derived(Base)%3A%0A%20%20%20%20def%20foo(self%2C%20x%3A%20Self)%20-%3E%20None%3A%0A%20%20%20%20%20%20%20%20x.bar()%0A%0A%20%20%20%20def%20bar(self)%20-%3E%20None%3A%0A%20%20%20%20%20%20%20%20pass%0A%0Adef%20test(a%3A%20Base%2C%20b%3A%20Base)%20-%3E%20None%3A%0A%20%20%20%20a.foo(b)%0Atest(Derived()%2C%20Base())) (the sandbox version) would accept this program. The only type checker I know of that is willing to reject the program is the internally deployed version (more up-to-date than sandbox version) of Pyre1, albeit with a terrible error message:

```python
8,4: Inconsistent override [14]: `Derived.foo` overrides method defined in `Base` inconsistently. Parameter of type `Variable[_Self_Derived__(bound to Derived)]` is not a supertype of the overridden parameter `Variable[_Self_Base__ (bound to Base)]`.

```

* * *

If we look at the typing spec, both [PEP 673](https://peps.python.org/pep-0673/) and the [typing spec site](https://typing.python.org/en/latest/spec/generics.html#use-in-method-signatures) dictates that the `Self` type should be semantically equivalent to a typevar with the declaring class as its bound. One consequence of this is that type checkers are supposed to make the same accept/reject decision on both the previous example that uses `Self` annotation, and the following example that uses bounded typevar:

```python
class Base:
    def foo[T: Base](self: T, x: T) -> None:
         pass

class Derived(Base):
    def foo[T: Derived](self: T, x: T) -> None:
        x.bar()

    def bar(self) -> None:
        pass

def test(a: Base, b: Base) -> None:
    a.foo(b)
test(Derived(), Base())

```

Interestingly, [mypy](https://mypy-play.net/?mypy=latest&python=3.12&gist=9fbac6c24355bf0e2cdee44e59b10d0b&flags=strict) still accepts this version of the program, whereas [pyright](https://pyright-play.net/?pyrightVersion=1.1.374&code=MYGwhgzhAEBCkFMBcBYAUNT0AmCBm0eA9kQNoAqSciAugBQQIh5XkA00AHqwJTQC0APmgA5IgDtk6LDMwAHSBHTpQi6ABEEAJwCWANwTY68Rj1QYsuAsTKUN2-YfqNmrDt2jk%2BQ0RKkXZLgA6ACMwLToeZQCcfGgwiJc8b2ExSXNA%2BUVoq2gAFwQIPLowKhMEDhCyxBTfdOksMCCbOhCotAKiuk1dAyMeDnLIniA) starts to reject it. Pyre1 (the internal version) would reject the program, with similar-looking error message as mentioned above.

Note that from a soundness perspective, none of the examples above is type-safe since they all cause runtime crashes. So it’s interesting to see that 3 different type checkers made 3 different choices on what to do here: mypy chose to be unsound but spec-conforming; pyright choose to be unsound and non-spec-conforming, and pyre1 (internal) is both sound and spec-conforming (but emits incomprehensible error messages). Normally such divergence may not be a big issue if uses of the `Self` type in similar manners are uncommon in practice. But a [quick search in typeshed](https://github.com/search?q=repo%3Apython%2Ftypeshed+%22%3A+Self%22&type=code) brings back more hits than I expected, which may indicate that the problem might not be that rare. So I figured it might be useful to at least raise the issue here and get some opinions (hopefully a consensus) about what to do with it.

* * *

If we take a step from the examples back and take a more general perspective, my understanding is that if the PEP 673 semantics (i.e. self type = typevar bounded by the containing class) is to be respected, then the use of `Self` annotation can only be covariant if we still want to maintain the substitution principle. In other words, if the `Self` annotation appears in a contravariant position (e.g. as type annotation of an argument), the theoretically sound way to handle it should be to either emit a type error, or treat the `Self` as exactly the same as the containing class (i.e. make it invariant – this is more lenient than erroring immediately on contravariant `Self` usage, since it could still accepts the safe case where even though contravariant `Self` appears, the method never gets overridden).

Another approach we could take here is to modify the spec to give up PEP 673 semantics, and just declare that `Self` always behaves covariantly even if it’s put at contravariant positions (i.e. aligning the spec with what mypy’s implementation today). Soundness will be unavoidably broken if we take this option so I hope we would take extra care of documenting this decision well. But it might be the path of least resistance.

---

<div class="post-metadata">

**Author:** ![grievejia](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/grievejia/32/18850_2.png) [@grievejia](https://discuss.python.org/u/grievejia)\
**Post date:** [March 28, 2025, 10:34pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/2 "2025-03-28T22:34:21Z")

</div>

On a separate note: if we look at those contravariant usages of `Self` in typeshed, a large number of use cases seem to aim at providing some kind of strongly-typed binary method. As an example, here’s an attempt to define a strongly-typed “equal” method:

```python
class Interface:
    def typed_equal(self, other: Self) -> bool:
        raise NotImplementedError()

class Derived0(Interface):
    def typed_equal(self, other: Self) -> bool: ...
class Derived1(Interface):
    def typed_equal(self, other: Self) -> bool: ...

def test(d0: Derived0, d1: Derived1) -> None:
    # User expects type checkers to accept these
    d0.typed_equal(d0)
    d1.typed_equal(d1)
    # User expects type checkers to reject these
    d0.typed_equal(d1)
    d1.typed_equal(d0)

```

The goal here is to have the `typed_equal()` method would enforce that the two arguments are of the same subclass of `Interface`. But as mentioned before, code like this does break substitution principle and does open up the possibility of runtime crashes regardless of how we define `Self` semantics or whether we count contravariant `Self` arguments as errors or not. In fact, [this](https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=e945fd19f7b54ccab7392f94dc303592d6e4fce2) well-cited research paper suggests that for binary methods, basic inheritance rule and basic subtyping rule are inherently in conflict with each other. The `typed_equal()` function may not be possible to write in a standard OOP system in a type-safe way.

If code change is an option, then one pattern found in other OOP languages for implementing type-safe binary operator is this:

```python
class Interface[T: Interface[T]]:
    def typed_equal(self: T, other: T) -> bool:
        raise NotImplementedError()

class Derived0(Interface[Derived0]):
    def typed_equal(self: Derived0, other: Derived0) -> bool: ...
class Derived1(Interface[Derived1]):
    def typed_equal(self: Derived1, other: Derived1) -> bool: ...

def test(d0: Derived0, d1: Derived1) -> None:
    # These should type check
    d0.typed_equal(d0)
    d1.typed_equal(d1)
    # These shouldn't type check
    d0.typed_equal(d1)
    d1.typed_equal(d0)

```

This pattern has different names in different languages (it’s called [CRTP](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern) in C++ and [F-Bounded type](https://dcapwell.github.io/scala-tour/F-Bounded%20Type.html) in Scala. Java’s `Comparable` and C#'s `IComparable` interface are also similar though they choose to bound their generic parameters differently), but they share the basic idea of making the base class generic, and then having subclasses “inject” themselves into the type argument of the base class. The weird-looking bound `T: Interface[T]` (often referred to as an “F-bound”) is essential to make the base class type check, but no type checkers today have support for it so the same kind of trick can’t be easily applied in Python.

---

<div class="post-metadata">

**Author:** ![mikeshardmind](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/mikeshardmind/32/14381_2.png) [@mikeshardmind](https://discuss.python.org/u/mikeshardmind)\
**Post date:** [March 29, 2025, 1:16am UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/3 "2025-03-29T01:16:45Z")

</div>

My analysis of this is that the issue is (and where an error message should be emitted) is at overriding the method foo in the subclass, and I agree that what Self _is_ does mean that where a `Self` annotation is contravariant, overriding it with a new `Self` annotation shouldn’t be allowed, the way to write that which works for comparison (both runtime and presumably all type checkers agree on this one)

Code sample in [pyright playground](https://pyright-play.net/?pyrightVersion=1.1.374&code=MYGwhgzhAEBCkFMBcBYAUNT0AmCBm0eA9kQNoAqSciAugBQQIh5XkA00AHqwJTQC0APmgA5IgDtk6LDMwAHSBHTpQi6ABEEAJwCWANwTY68Rj1QYsuAsTKVqjeo2asO3aOT5DREqRdkbtfUMAOgAjMC06Th5pLFjMAAEIABcwZJ1gAFsEZIALImx4nHxocMiiKhMET2ExSXN-eUVlNCtoZIQUujBKxA5Q3tMBWp8GrDBgmzpQmLR0Dq7NXQMjHg4quh4eIA)

```python
class Base:
    def foo[T: Base](self: T, x: T) -> None:
         pass

class Derived(Base):
    def foo[T: Base](self: T, x: T) -> None:
        Derived.bar(x)
    
    @staticmethod
    def bar(o: Base) -> None:
        pass

def test(a: Base, b: Base) -> None:
    a.foo(b)

test(Derived(), Base())

```

or go even further and make bar a function rather than a method of a subclass.

---

<div class="post-metadata">

**Author:** ![rchen152](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/rchen152/32/16955_2.png) [@rchen152](https://discuss.python.org/u/rchen152)\
**Post date:** [March 29, 2025, 3:50am UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/4 "2025-03-29T03:50:07Z")

</div>

Jia, thanks for the detailed post! I agree that ideally, type checkers should reject all three examples you showed (with concrete parameter types, with `Self`, and with a bounded TypeVar), as they are all unsafe in the same way.

The fact that typeshed relies so much on the `Self` pattern makes this trickier. IMO we’d need to come up with an alternative that we can move typeshed to, and suggest for third-party libraries/stubs, before we can consider rejecting this.

---

<div class="post-metadata">

**Author:** ![erictraut](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/erictraut/32/15190_2.png) [@erictraut](https://discuss.python.org/u/erictraut)\
**Post date:** [March 29, 2025, 7:51pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/5 "2025-03-29T19:51:06Z")

</div>

Thanks for the post Jia. I agree that all three code samples above should be flagged as an error by a type checker. Pyright failed to detect the error in the second code sample because of a bug in my override consistency check. The [fix](https://github.com/microsoft/pyright/commit/4e847ba729b3722005d745d82c80816471db38ef) is simple, and it will be included in the next release of pyright.

This bug fix results in about two dozen new errors within the typed code bases covered by our regression tests. I examined all of these errors to convince myself that I wasn’t introducing any new type checker bugs. All of these new errors appear to be legitimate and highlight bugs either in the implementation or in the type annotations. Most of them will be trivial to fix.

All of these new errors fell into one of three categories where `Self` was used: 1) in a method parameter annotation (hence contravariant), 2) in a return type annotation as a type argument to an invariant class, and 3) in the annotation for a mutable attribute (hence invariant).

```python
class Parent:
    # Category 1
    def method1(self, x: Self) -> None: ...

    # Category 2
    def method2(self) -> list[Self]: ...

    # Category 3
    attr: Self

```

I’m not concerned about reporting new diagnostics in typeshed stubs. The maintainers of typeshed are typing experts, and they understand the tradeoffs that are required for stdlib. If you look in the stdlib stubs, you’ll see that it’s not uncommon to see `# type: ignore` and `# pyright: ignore` comments.

My bigger concern here would be effects on code that overrides methods defined in typeshed stdlib stubs. Based on the regression test results from my change, this doesn’t appear to be a widespread problem. I didn’t see any examples of this.

---

<div class="post-metadata">

**Author:** ![rchen152](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/rchen152/32/16955_2.png) [@rchen152](https://discuss.python.org/u/rchen152)\
**Post date:** [March 29, 2025, 10:33pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/6 "2025-03-29T22:33:06Z")

</div>

Glad to hear that for pyright, this was a simple oversight with an easy fix.

My concern re: typeshed was more about whether type checkers would start erroring on a common, useful pattern without a clear alternative. Since you didn’t find this to be a widespread problem (and thank you for looking into this, Eric!), I’m now much less concerned 🙂

---

<div class="post-metadata">

**Author:** ![rchen152](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/rchen152/32/16955_2.png) [@rchen152](https://discuss.python.org/u/rchen152)\
**Post date:** [March 29, 2025, 11:33pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/7 "2025-03-29T23:33:21Z")

</div>

I started looking into updating [this Protocol conformance test](https://github.com/python/typing/blob/5f97c008e9bc45b9611c2401937ba247df4cf828/conformance/tests/protocols_self.py#L72), as I believe `C2` can’t be allowed to match `P2Parent` in that test for the same reason that the method overrides above should be rejected (here’s a [modification](https://codapi.org/embed/?sandbox=python&code=data%3A%3Bbase64%2CjY%2FPCsIwDIfvfYocG6h7gB08iGcVfIK6pTroltGU0b29rNtE8Q%2FmlJBfvo%2B4wC3EsW%2B6KzRtzyHCKXDkir2BM3mnVOWtCOyskF5XWCoAgJocOGYt5J2BVOYDhM0WDtzRnMnVW5EVtKfQDFT%2FIrwBUnGxQaN63EyjfJY9u47xRuEP4TdKTutU5u8NjHOzfJ%2BKCTSicnpRaDQvSo14Bw%3D%3D) of one of Jia’s examples showing that we can get into the same problematic situation using protocols).

But I realized that the spec actually [says](https://typing.python.org/en/latest/spec/generics.html#use-in-protocols):

> If a protocol uses `Self` in methods or attribute annotations, then a class `Foo` is assignable to the protocol if its corresponding methods and attribute annotations use either `Self` or `Foo` or any of `Foo` ’s subclasses.

which matches the current conformance test behavior. This seems to me like a mistake/oversight that should be fixed.

Before I continue down this route, is there any reason I’m missing for why `Self` should be treated differently in protocols?

---

<div class="post-metadata">

**Author:** ![ilevkivskyi](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/ilevkivskyi/32/26_2.png) [@ilevkivskyi](https://discuss.python.org/u/ilevkivskyi)\
**Post date:** [March 31, 2025, 3:09pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/8 "2025-03-31T15:09:08Z")

</div>

For mypy it was a very conscious decision (made long time ago), this is why it consistently accepts both `Self`-type based example, and the equivalent one with regular type variables. The reason is that handling self-types safely would make them a huge pain to use (in particular in protocols, but also in regular classes).

---

<div class="post-metadata">

**Author:** ![erictraut](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/erictraut/32/15190_2.png) [@erictraut](https://discuss.python.org/u/erictraut)\
**Post date:** [March 31, 2025, 9:50pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/9 "2025-03-31T21:50:01Z")

</div>

Consistent with what Ivan said, I think this comes down to a tradeoff between safety and practicality. Consider the following examples where `Self` appears in a contravariant or invariant position within a protocol definition.

```python
from __future__ import annotations
from typing import Protocol, Self

class Contra:
    def method(self, a: Self) -> None: ...

class ContraProto(Protocol):
    def method(self, a: Self) -> None: ...

a: ContraProto = Contra() # Allowed?

class Inv:
    def method(self) -> list[Self]: ...

class InvProto(Protocol):
    def method(self) -> list[Self]: ...

b: InvProto = Inv() # Allowed?

```

@ilevkivskyi, is this what you had in mind when you said that it would be a huge pain to use? Can you think of other usage patterns that would suffer?

I did a quick experiment with pyright where I changed it to enforce type safety for protocols that use `Self`. I expected `mypy_primer` to reveal a bunch of new errors across many typed code bases, but the fallout was surprisingly small. Based on this experiment, I’d be in favor of changing the spec to enforce safety when `Self` is used in protocols.

In any event, the current wording in the spec seems confusing and ambiguous. It makes sense in a covariant context, but it doesn’t make sense in a contravariant or invariant context. If we decide not to amend the spec in favor of type safety, we should at least improve the wording so it’s less ambiguous and explain why the spec chooses to allow unsafe behavior in this case.

---

<div class="post-metadata">

**Author:** ![ilevkivskyi](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/ilevkivskyi/32/26_2.png) [@ilevkivskyi](https://discuss.python.org/u/ilevkivskyi)\
**Post date:** [April 1, 2025, 8:19am UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/10 "2025-04-01T08:19:54Z")

</div>

All examples ultimately boil down to something like that, but it has many consequences (again, not just for protocols). For example:

- Some generic classes that may have been covariant, will be forced to be invariant
- Two classes with “problematic” methods can’t be used in multiple inheritance
- `Self`-types in variable annotations (one of the selling point of PEP 673) become (almost) moot, since this is unsafe:

```python
class B:
    x: Self
class C(B):
    other = 1

```

Another problem (IIRC this is where discussion got stuck last time) is if we prohibit this, where to give the error? For protocols it should be obviously at the definition site, but for regular classes it is not so obvious. Technically, having `Self` in a contravariant position is safe unless you actually override a method, but then some library authors may want to be warned that their API is “problematic” (i.e. impossible to override).

Anyway, I am not really interested in discussing this all again, just adding my 5 cents.

---

<div class="post-metadata">

**Author:** ![grievejia](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/grievejia/32/18850_2.png) [@grievejia](https://discuss.python.org/u/grievejia)\
**Post date:** [April 1, 2025, 6:02pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/11 "2025-04-01T18:02:43Z")

</div>

If the decision were to reject contravariant `Self`s, then I would propose we error at definition site, unless the method is decorated with `@final` (note that this is not an argument on whether or not we should reject contravariant `Self`s – I’m just trying to address Ivan’s concern of “where to error” if we were to take the rejection route). E.g.

```python
class Foo:
  def foo(self, x: Self) -> None: ... # E: It's not safe to use `Self` at this position. Use `Foo` instead or mark the method as final.

class Bar(Protocol):
  def bar(self, x: Self) -> None: ... # E: It's not safe to use `Self` at this position. Use `Bar` instead or mark the method as final.

class Baz:
  @final
  def baz(self, x: Self) -> None: ... # OK

```

This may appear overly restrictive at first glance. But I think it can be argued that such restriction is a good thing from tool usability perspective as it would timely warn the users that the type annotation may not mean what they thought it would mean. When writing down this kind of code, my guess is that the authors of the code almost always expect that the `Self` type could still behave covariantly in subclasses. Def-site errors has the capability to warn the users immediately (while they are writing down the code) that such assumption is generally unsafe to make, preventing the users from going down the path further.

Actionable suggestions can also be easily offered in the error message, nudging the users to either replace `Self` with the containing class or marking the method with `@final`. If neither suggestions apply, i.e. the user really wants to implement a strongly-typed binary operator, then def-site error can still serve as a natural place to put in links to documentations explaining how strongly-typed binary operator is not implementable in the current type system (until a future type system extension could offer a solution there).

* * *

Regarding `Self` annotations in class attributes, anecdotally I remember it was a pretty late revision when PEP 673 were drafted, and I do regret today for not flagging it for further discussions at that time. One additional piece of context though was that back when the PEP was drafted, there was no typing spec or conformance tests, and it was up to each type checker to decide whether they want to reject covariant mutable attribute definitions. e.g.

```python
class A: pass
class B(A): pass

class C:
  x: A
class D(C):
  x: B # Should this be an error?

```

Back then most type checkers did not (and still do not) choose to emit an error here. So it can be argued that allowing covariant self attribute definition at least did not make the unsoundness issue worse, and it’s fine to include that late revision back then.

If we were to be 100% consistent and sound, then yes I do think that we need to both reject `Self` annotation on mutable attributes (i.e. removing that part of PEP 673) and reject covariant mutable attributes. If we want to keep PEP 673 intact, then we’d have to make a choice on whether to give up consistency (i.e. allow covariant `Self` attributes but not covariant mutable attributes, or the other way around), or to give up soundness (i.e. allow both covariant `Self` attribute and covariant mutable attributes).

* * *

There were 2 other concerns mentioned (regarding generic classes and multiple inheritance) but I don’t think I understand the underlying issues well enough to comment. Happy to follow up if I get access to some concrete examples demonstrating the problems!

---

<div class="post-metadata">

**Author:** ![beauxq](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/beauxq/32/15042_2.png) [@beauxq](https://discuss.python.org/u/beauxq)\
**Post date:** [April 1, 2025, 7:37pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/12 "2025-04-01T19:37:08Z")

</div>

> [@grievejia](#):
>
> it was up to each type checker to decide whether they want to reject covariant mutable attribute definitions. e.g.
> 
> ```python
> class A: pass
> class B(A): pass
> 
> class C:
> x: A
> class D(C):
> x: B # Should this be an error?
> 
> ```
> 
> Back then most type checkers did not (and still do not) choose to emit an error here.

I was happy to see it when pyright started emitting an error for this.  
It helped me see things that were unsafe, that I didn’t see before.  
And [PEP 767 – Annotating Read-Only Attributes | peps.python.org](https://peps.python.org/pep-0767/) is looking to make it so we can use this pattern more safely.

---

<div class="post-metadata">

**Author:** ![carljm](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/carljm/32/959_2.png) [@carljm](https://discuss.python.org/u/carljm)\
**Post date:** [September 8, 2025, 6:21pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/13 "2025-09-08T18:21:29Z")

</div>

I think there may be some unstated assumptions here about how `Self` can or can’t be solved when a (non-overridden) super-type method is called from an instance of a sub-type.

If we consider this example:

```python
from typing import Self

class Base:
    def method(self, other: Self) -> Self: ...

class Sub(Base): ...

Sub().method(Base())

```

Mypy and pyright both consider this call invalid, because they insist that `Self` in `Sub().method` must have an upper bound of `Sub`. I don’t think this is right. `method` is defined on `Base`, and its body is type-checked assuming `Self` has an upper bound of `Base`, so it is perfectly safe to allow the call `Sub().method(Base())` to type-check, and to solve its return type as `Base`.

With the current behavior, the `other: Self` annotation in `Base.method` becomes a Liskov violation as soon as `Base` is inherited, and this is a problem: it means we have to disallow all use of `Self` in contravariant position on non-final classes, and I don’t think we should do that.

But if we allow the call `Sub().method(Base())` to type-check, and allow solving `Self` to `Base` in that case, it makes correctly enforcing variance on `Self` much less problematic. There is only a problem if `Sub` actually overrides `method`, and its override also annotates `other: Self` – this specific override is what should be flagged as unsafe. (It would be a safe override if instead it has `other: Base`).

Note that allowing `Sub().method(Base())` to solve to `Base` doesn’t break the more common use of a `Self` return type; `Sub().method(Sub())` should still solve to `Sub`, and similarly if we had `def other_method(self) → Self` on `Base`, `Sub().other_method()` should of course solve to `Sub`. We don’t have to enforce `Sub` as an upper bound (wrongly, IMO) in order to solve to `Sub` when the call allows that.

---

<div class="post-metadata">

**Author:** ![samwgoldman](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/samwgoldman/32/17714_2.png) [@samwgoldman](https://discuss.python.org/u/samwgoldman)\
**Post date:** [September 8, 2025, 6:41pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/14 "2025-09-08T18:41:01Z")

</div>

I think users will commonly expect that, for any method returning `Self`, calling that method returns the type of the receiver, regardless of where that method was defined. But if we admit `Sub().method(Base())`, then the type of that expression can’t be `Sub`, it has to be at least `Base`.

For a more realistic example, consider a “builder” pattern, where a parent class implements some common methods (all returning `Self`) and a child class implements some specific builder methods (also returning `Self`). Users would like to be able to chain these methods.

---

<div class="post-metadata">

**Author:** ![carljm](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/carljm/32/959_2.png) [@carljm](https://discuss.python.org/u/carljm)\
**Post date:** [September 8, 2025, 6:42pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/15 "2025-09-08T18:42:59Z")

</div>

I think I addressed this in my last paragraph. In all of those common use cases, the return type can and should still be solved to `Sub`, regardless of where the method is defined. This is just typevar solving as it should normally operate: the upper bound is not also a lower bound!

But in the less-common case where there is another argument also typed as `Self`, we should be willing to consider the types of all `Self` arguments when solving (as we would when solving any other typevar), and not enforce an overly-strict upper bound on the type of `Self` based on privileging one argument typed as `Self` over the others.

---

<div class="post-metadata">

**Author:** ![samwgoldman](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/samwgoldman/32/17714_2.png) [@samwgoldman](https://discuss.python.org/u/samwgoldman)\
**Post date:** [September 8, 2025, 6:56pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/16 "2025-09-08T18:56:42Z")

</div>

You’re right. Apologies for not reading more closely.

---

<div class="post-metadata">

**Author:** ![oscarbenjamin](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/oscarbenjamin/32/1209_2.png) [@oscarbenjamin](https://discuss.python.org/u/oscarbenjamin)\
**Post date:** [September 8, 2025, 8:33pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/17 "2025-09-08T20:33:14Z")

</div>

What is the alternative to using `Self` in a protocol that is supposed to be invariant?

I don’t see how to do this with type variables:

```python
from typing import Protocol, Self

class Addable(Protocol):
    def __add__ (self, other: Self, /) -> Self:
        ...

def add[E: Addable](a: E, b: E) -> E:
    return a + b

add(1, 2)

# checkers apparently allow this:
add(1, [])

```

---

<div class="post-metadata">

**Author:** ![samwgoldman](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/samwgoldman/32/17714_2.png) [@samwgoldman](https://discuss.python.org/u/samwgoldman)\
**Post date:** [September 9, 2025, 4:57pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/18 "2025-09-09T16:57:27Z")

</div>

Reflecting on this a bit, I have a maybe helpful way of looking at the proposal, which might help us better understand the implications / safety properties. The key thing is, if we think of `Self` like a type parameter, how we consider it’s scope.

PEP 673 does give a translation to TypeVar. The translation described there supports Carl’s proposed behavior – the `Self` type parameter is bound at the method, so it is “solved” on method calls.

The behavior we see from mypy, pyright, and pyrefly, seem better described by a `Self` type parameter on the _class_ itself. That is, instead of this translation:

```python
class A:
  def f[Self: 'A'](self: Self) -> Self: ...
  def g[Self: 'A'](self: Self, other: Self) -> Self: ...
class B(A):
  ...

o = B()
o.f() # B <: Self
o.g(A()) # A | B <: Self

```

We have something more like this:

```python
class A[Self: 'A']:
  def f(self: Self) -> Self: ...
  def g(self: Self, other: Self) -> Self: ...
class B[Self: 'B'](A[Self])
  ...
o = B() # Self = B
o.f() # no check
o.g(A()) # error: A </: B

```

I think both are reasonable interpretations of `Self`. The former has the benefits that (1) it is what was actually described and accepted in the PEP and (2) it allows for solving `Self` to `A` when in `o.g(A())`, giving a safe interpretation to contravariant occurrences of `Self`.

The latter is honestly more intuitive for me, but taking this interpretation means that contravariant occurrences of `Self` are just unsound. This unsoundness is unfortunate, but it seems like mypy at least has knowingly accepted it.

Hopefully this framing is helpful.

Building on this framing, I have a question about `Self` bound on methods: it gives a clear definition of how we should think of `Self` occurrences in methods, but what about `Self` occurrences in attributes? In the above example, the methods `f` and `g` have different instantiations for `Self`, but both could access a hypothetical attribute `attr: list[Self]`. If `g` writes `other` into `attr`, then how can `f` safely read from it?

---

<div class="post-metadata">

**Author:** ![mikeshardmind](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/mikeshardmind/32/14381_2.png) [@mikeshardmind](https://discuss.python.org/u/mikeshardmind)\
**Post date:** [September 9, 2025, 8:06pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/19 "2025-09-09T20:06:03Z")

</div>

If you apply the normal rules for typevariable solving (desugar it back to what it would be without Self existing), it’s obvious that the presence of such an attribute makes it invariantly the exact class the attribute is defined in. The obvious problem here with use of Self then becomes if there are any class-scoped uses of Self, all uses of Self must be considered class-scoped because there is no way to differentiate between a method-scoped Self and class-scoped (I would go further and argue all uses of Self should be class scoped, and if you want method scoped, use a typevariable)

This is sound, but leads to issues if further subclassed, subclasses can no longer use Self when referring to this or anything that needs to align with that attribute.

Personally, I’d rather this be sound. It’s been my experience that the cases where this would be a problem are places people should have either written a function that can accept any instance of the class, rather than methods, or should have marked a class as final anyhow. Either of these options sufficiently prevents variance issues, and lack of either of them leads to real bugs.

---

<div class="post-metadata">

**Author:** ![carljm](https://sea2.discourse-cdn.com/flex002/user_avatar/discuss.python.org/carljm/32/959_2.png) [@carljm](https://discuss.python.org/u/carljm)\
**Post date:** [September 10, 2025, 5:59pm UTC](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338/20 "2025-09-10T17:59:39Z")

</div>

Thanks, I do think “scope of `Self`” is a clear way to understand the options here.

The only interpretation of `Self` typed attributes I can see that is both useful and sound is simply as syntactic sugar for the name of the enclosing class, with no implied specialization in subclasses. That is:

```python
from typing import Self

class A:
    x: Self

class B(A):
    pass

reveal_type(B().x) # revealed: A

```

But mypy and pyright both reveal `B` here, which is unsound (since any given instance of `B` could be typed as `A` at some point in the program, and have an `A` instance assigned to its `x` attribute.)

I’m curious how many real-world uses of `Self` attributes are specifically looking for this unsound subclass specialization, vs just looking for a nice-looking way to spell “the current class” without having to quote the annotation.

[Next page](https://discuss.python.org/t/unsoundness-of-contravariant-self-type/86338.md?page=2)
