2026, Jan 01 03:00
typing.Self vs class attributes in Python: mypy limitation and a Generic TypeVar workaround
Learn why typing.Self fails for class-level attributes in Python with mypy, and how a bound TypeVar + Generic provides precise, inheritance-safe typing today.
When you want an instance attribute to hold a reference to the instance itself and keep the type precise across inheritance, the natural instinct in modern Python is to reach for typing.Self. The surprise comes when a static checker accepts Self in method signatures but rejects it in class-level attribute annotations. Let’s unpack why that happens and how to work around it cleanly without sacrificing type safety.
The minimal example
The straightforward approach that ties an attribute to the concrete class works, but it hardcodes the class name and doesn’t capture subclassing semantics:
class Gadget:
anchor: "Gadget"
def attach(self) -> None:
self.anchor = self
Trying to switch the attribute to Self to make it inheritance-friendly looks more elegant, but this is where mypy pushes back:
from typing import Self
class Gadget:
anchor: Self
def attach(self) -> None:
self.anchor = self
Static analysis reports an incompatible assignment: the expression is seen as the concrete class type while the attribute is treated as Self, which mypy considers differently at the class attribute level.
What’s going on under the hood
PEP 673 introduced typing.Self in Python 3.11 to express methods that return the exact type of the current class, enabling precise typing through inheritance. For example:
from typing import Self
class Gadget:
def duplicate(self) -> Self:
...
That’s the supported sweet spot: method signatures. The catch is that mypy currently special-cases Self in method signatures, not in class-level attribute annotations. As a result, annotating an attribute with Self does not play well with assigning self to it; mypy treats the attribute’s Self as a late-bound type distinct from the concrete class instance it infers for self. Annotating the parameter as Self won’t fix an attribute-level Self either, because the special handling applies to method returns, not class attributes. This behavior is tracked in mypy’s issue list; see mypy#14075.
A practical workaround with TypeVar + Generic
To keep attribute annotations precise across inheritance today, you can rely on a TypeVar bound to the base class and make the class generic over that TypeVar. This restores the relationship between the attribute type and the self type in a way mypy understands:
from typing import Generic, TypeVar
S = TypeVar("S", bound="Gadget")
class Gadget(Generic[S]):
anchor: S
def attach(self: S) -> None:
self.anchor = self
Here, anchor is typed as exactly S, the same type as self. In subclasses, S resolves to the subclass, and mypy accepts the assignment without conflict. If you prefer a minimal change and can live without the late-bound semantics, simply keeping the class name in the annotation is also acceptable.
Why this matters
Precise typing of instance attributes is not cosmetic; it affects how safely you can refactor and extend class hierarchies. When Self in attribute annotations isn’t recognized, you risk either false positives that clutter reviews or, worse, loosening types to silence a checker and losing guarantees. The generic TypeVar approach preserves intent and correctness today while aligning with how Self is meant to be used in methods.
Takeaways
Use typing.Self where it is reliably supported: method signatures, especially returns, as introduced by PEP 673. For class-level attributes that should mirror the dynamic type of self, prefer a bound TypeVar with a generic base class. If you only need the attribute to be the base class type, annotate it with the class name directly. Keep an eye on the ongoing work in mypy to improve late-bound attribute annotations.