Methods that return their own instance are common in builders, fluent APIs, configurable objects, and context managers. Annotating those returns with the class name works until a subclass appears: the type checker may lose the more specific subtype. typing.Self means “the current concrete class,” preserving subclass types without a manual bound TypeVar.
This guide explains Self in instance methods, classmethods, alternate constructors, clone operations, context managers, protocols, generic classes, and inheritance. It also shows when Self is the wrong annotation and how it differs from explicit generics.
The problem with returning the class name
class Query:
def limit(self, amount: int) -> "Query":
self._limit = amount
return self
class SqlQuery(Query):
def order_by(self, field: str) -> "SqlQuery":
self._order = field
return self
query = SqlQuery().limit(10)
# A checker may now see Query instead of SqlQueryThe implementation returns the same concrete instance, but the fixed annotation promises only Query. Fluent chaining can therefore lose subclass-specific methods.
Using Self
from typing import Self
class Query:
def limit(self, amount: int) -> Self:
self._limit = amount
return self
class SqlQuery(Query):
def order_by(self, field: str) -> Self:
self._order = field
return self
query = SqlQuery().limit(10).order_by("name")Self is interpreted according to the concrete receiver type, so the result of limit() remains SqlQuery.
Fluent APIs
Builders often mutate the object and return self for chaining.
class Request:
def __init__(self) -> None:
self._headers: dict[str, str] = {}
self._timeout = 5.0
def header(self, name: str, value: str) -> Self:
self._headers[name] = value
return self
def timeout(self, seconds: float) -> Self:
if seconds <= 0:
raise ValueError("timeout must be positive")
self._timeout = seconds
return selfA subclass can add methods without losing them after inherited fluent calls.
Self in classmethods
Self also describes a factory that returns an instance of the class on which it was called.
class Document:
def __init__(self, text: str) -> None:
self.text = text
@classmethod
def empty(cls) -> Self:
return cls("")
class MarkdownDocument(Document):
pass
markdown = MarkdownDocument.empty()The inferred type of markdown is MarkdownDocument, provided the implementation actually constructs cls.
When a factory always returns the base class
Do not use Self if the method always constructs a specific base class regardless of the subclass.
class Document:
@classmethod
def default(cls) -> "Document":
return Document("template")Annotating this method with Self would falsely promise that MarkdownDocument.default() returns a MarkdownDocument.
Alternate constructors and subclass constructors
class Vector:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
@classmethod
def origin(cls) -> Self:
return cls(0.0, 0.0)
@classmethod
def from_iterable(cls, values) -> Self:
x, y = values
return cls(float(x), float(y))Subclasses must keep a compatible constructor or override the factory. Self describes the return relationship, but it cannot guarantee at runtime that every subclass accepts the same constructor arguments.
Clone and copy methods
from copy import copy
class Configuration:
def clone(self) -> Self:
return copy(self)If copy(self) preserves the concrete class, Self accurately describes the result. The method may return the same instance or a new one; Self expresses the type, not object identity.
Self in parameters
Self can also appear as a parameter type when an operation requires another object of the same concrete class.
class Point:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
def distance_to(self, other: Self) -> float:
dx = self.x - other.x
dy = self.y - other.y
return (dx * dx + dy * dy) ** 0.5This is more restrictive than accepting any Point. Use Self only when the relationship between the receiver and the argument is real. If subclasses can interact freely with base-class instances, annotate the base class instead.
Self in properties
A property may return an instance of the same concrete class.
class Node:
@property
def root(self) -> Self:
current = self
while current.parent is not None:
current = current.parent
return currentThis annotation is correct only if every parent in the chain is guaranteed to have the same concrete type. Heterogeneous trees may need a base-class return type.
Context managers
__enter__ commonly returns self.
class Session:
def __enter__(self) -> Self:
self.open()
return self
def __exit__(self, exc_type, exc, tb) -> None:
self.close()
class AuditedSession(Session):
def event(self, text: str) -> None:
...
with AuditedSession() as session:
session.event("started")Self preserves AuditedSession inside the with block.
Self in Protocol
A protocol can require fluent behavior.
from typing import Protocol, Self
class Configurable(Protocol):
def configure(self, key: str, value: object) -> Self:
...A compatible implementation must return its own concrete type. The guide to Python Protocol explains structural typing in more detail.
Self in generic classes
Self represents the concrete class together with its generic specialization.
from typing import Generic, TypeVar, Self
T = TypeVar("T")
class Box(Generic[T]):
def __init__(self, value: T) -> None:
self.value = value
def replace(self, value: T) -> Self:
self.value = value
return selfFor Box[int], replace() returns the same specialized type.
When a method changes the generic parameter
Self is not appropriate when the result has different type parameters.
from collections.abc import Callable
U = TypeVar("U")
class Box(Generic[T]):
def map(self, function: Callable[[T], U]) -> "Box[U]":
return Box(function(self.value))map() creates a Box[U], not necessarily the same type as self. Explicit generics express this transformation correctly.
Comparison with a bound TypeVar
Before Self, code often used a bound TypeVar tied to the receiver.
from typing import TypeVar
TQuery = TypeVar("TQuery", bound="Query")
class Query:
def limit(self: TQuery, amount: int) -> TQuery:
self._limit = amount
return selfSelf is shorter and clearer for this common pattern. A TypeVar remains useful when the same relationship spans multiple functions, several parameters, or external types.
Subclass overrides must preserve the contract
class Base:
def normalize(self) -> Self:
return self
class Special(Base):
def normalize(self) -> Base:
return Base()A type checker should report the incompatible override. A base method returning Self promises that subclasses also return their own concrete type.
Self does not mean the same object
from copy import copy
class Record:
def with_name(self, name: str) -> Self:
new = copy(self)
new.name = name
return newThe method returns a new object of the same concrete class. Document whether a fluent operation mutates the receiver or creates a copy.
Decorators and signature preservation
Poorly typed decorators can erase a method’s Self relationship. Use functools.wraps at runtime and ParamSpec or carefully defined generics when a decorator must preserve parameters and return types.
A decorator returning Callable[..., object] removes the useful fluent type information even if the wrapped function still behaves correctly.
Version compatibility
typing.Self is available in modern Python. Older supported versions can import it from typing_extensions.
try:
from typing import Self
except ImportError:
from typing_extensions import SelfLibraries should declare the dependency and test every supported Python version.
Common mistakes
- Using Self for a factory that constructs the base class: the promise is false for subclasses.
- Using Self when a generic parameter changes: describe the destination type explicitly.
- Restricting parameters unnecessarily: the base class may be the correct type.
- Assuming Self means identity: a new object of the same subtype is valid.
- Ignoring incompatible subclass constructors:
cls(...)may fail at runtime. - Using Self in a staticmethod: there is no receiver that defines the concrete Self type.
Complete example: query builder
from typing import Self
class Query:
def __init__(self, table: str) -> None:
self.table = table
self.filters: list[str] = []
self._limit: int | None = None
def where(self, expression: str) -> Self:
self.filters.append(expression)
return self
def limit(self, amount: int) -> Self:
if amount < 1:
raise ValueError("invalid limit")
self._limit = amount
return self
@classmethod
def from_table(cls, table: str) -> Self:
return cls(table)
class OrderedQuery(Query):
def order_by(self, field: str) -> Self:
self.order = field
return self
query = (
OrderedQuery.from_table("customers")
.where("active = true")
.limit(20)
.order_by("name")
)Every method preserves OrderedQuery, including the inherited classmethod factory.
When not to build a fluent API
Self improves typing but does not guarantee that chaining is the clearest design. Long chains can hide side effects and complicate debugging. Methods that perform I/O, commit data, or modify global state may be clearer when they return explicit results.
Use fluent methods for predictable configuration and transformation, and document mutability and failure behavior.
Conclusion
typing.Self represents the concrete class of the receiver. It simplifies fluent methods, alternate constructors, clones, context managers, protocols, and builders while preserving subclasses automatically.
The official Python Self documentation describes supported uses. Choose Self when a return or parameter truly follows the concrete receiver type, and use explicit generics or a base class when the relationship is different.







