typing.Self vs TypeVar for Return Types
Self (PEP 673, Python 3.11) is the clean way to type any method that returns its own instance — fluent builders, __enter__, copy(), alternative constructors. The old approach — a TypeVar bound to the class and threaded through self — still works and is what you need before 3.11, but it is verbose and easy to get subtly wrong so subclasses lose their precise type. Prefer Self; fall back to the bound TypeVar only for older runtimes.
Methods that return self (or a fresh instance of the same class) are everywhere: builder chains, context managers, clone() helpers, and classmethod factories. The hard part is making the return type track the actual subclass, so that RetryBuilder().with_backoff() is still a RetryBuilder and not just its base. This page belongs to Self and NotRequired Types and compares the two ways to express that.
Self and the subclass type survives the chain; return the base class and it collapses.The problem: returning self from a base class
Annotating the return type as the enclosing class name looks correct but is wrong for subclasses. The checker takes you literally: a method returning QueryBuilder returns exactly that, even when called on a subclass.
# Python 3.10, checked with mypy 1.10 / pyright 1.1.370
class QueryBuilder:
def where(self, clause: str) -> "QueryBuilder": # too concrete
return self
class PagedQuery(QueryBuilder):
def limit(self, n: int) -> "PagedQuery":
return self
PagedQuery().where("id > 0").limit(10) # mypy: [attr-defined] "QueryBuilder" has no attribute "limit"
where returns the base type, so .limit() — which only PagedQuery defines — is flagged with [attr-defined]. Every method in the chain has to preserve the concrete type for fluent APIs to type-check.
The old fix: a TypeVar bound to the class
Before PEP 673 the idiom was a TypeVar bound to the class, threaded through self. It works, but every such method must declare self: T and return T, which is noisy and easy to forget on one method in a long chain.
# Python 3.10, checked with mypy 1.10 / pyright 1.1.370
from typing import TypeVar
TBuilder = TypeVar("TBuilder", bound="QueryBuilder")
class QueryBuilder:
def where(self: TBuilder, clause: str) -> TBuilder:
return self
class PagedQuery(QueryBuilder):
def limit(self, n: int) -> "PagedQuery":
return self
PagedQuery().where("id > 0").limit(10) # now OK — where returns PagedQuery
This relies on generics and TypeVar machinery and a forward-referenced bound. Miss the self: TBuilder annotation on one method and the type collapses again with no obvious warning.
The clean fix: Self
Self says “the type of the current instance” without any TypeVar. It reads clearly, needs no per-method self annotation, and does the right thing in subclasses automatically.
# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
from typing import Self
class QueryBuilder:
def where(self, clause: str) -> Self:
return self
class PagedQuery(QueryBuilder):
def limit(self, n: int) -> Self:
return self
reveal_type(PagedQuery().where("id > 0")) # PagedQuery
PagedQuery().where("id > 0").limit(10) # OK
Self also shines on alternative constructors and __enter__:
# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
from typing import Self
class Session:
@classmethod
def open(cls, url: str) -> Self: # subclass factory keeps its type
return cls()
def __enter__(self) -> Self:
return self
On Python 3.9–3.10, import Self from typing_extensions for the same behaviour without upgrading the runtime.
Self is a typing-only construct: at runtime your method still just returns whatever object it returns. If a method annotated -> Self actually returns a *different* class's instance, Python runs it happily while mypy reports [return-value]. The annotation constrains the checker, not the interpreter.
Common mistakes
- Returning a hardcoded base class name: subclass-only methods later in the chain fail with
[attr-defined]. UseSelf(or the bound TypeVar) so the concrete type survives. - Returning a new base instance under
Self:return QueryBuilder()in a method typed-> Selfis[return-value](Incompatible return value type), because a base instance is not necessarilySelf. Returnselfortype(self)(...). - Forgetting
self: Ton one method of the TypeVar pattern: that method silently returns the base type and breaks the chain. This fragility is the main reason to preferSelf. - Using
Selfoutside a class body: it is only meaningful inside a class; at module scope mypy reports[misc]. For standalone functions use an explicit TypeVar.
FAQ
Is Self just shorthand for a bound TypeVar?
Semantically it behaves like an implicitly bound TypeVar on self, but it removes the boilerplate: no TypeVar declaration, no self: T on every method, no forward-referenced bound. Both mypy and pyright treat a correct Self and a correct bound-TypeVar version as equivalent at the call site.
Can I use Self in a classmethod or a nested type like list[Self]?
Yes to both. In a classmethod, -> Self means “an instance of the actual class the method was called on,” which is exactly what you want for factories. You can also nest it — def batch(self) -> list[Self] types a method returning a list of same-typed instances.