Using typing.Self for Fluent Interfaces in Python
Annotate each chainable method’s return type as Self (PEP 673, Python 3.11+; or typing_extensions.Self for 3.10). Static checkers then resolve the return to the concrete subclass at each call site — no TypeVar boilerplate, no base-class leakage. Never use Self on init or new.
Implementing chainable APIs in Python traditionally required verbose TypeVar bindings or inline type ignores. With Python 3.11, typing.Self provides a precise, PEP 673-compliant mechanism to annotate methods that return the instance itself. This guide details exact syntax for builder patterns, resolves common mypy and pyright inheritance errors, and demonstrates how to integrate this pattern into the broader Advanced Typing Patterns & Generics ecosystem. For developers managing complex class hierarchies, understanding typing.Self alongside Self and NotRequired Types ensures strict static analysis compliance without sacrificing API ergonomics.
Self resolves to AdvancedPipeline at every step; a legacy bound TypeVar would widen to DataPipeline.typing.Self has no runtime effect — Python does not enforce return types. The annotation exists purely for static checkers. At runtime, the method simply returns self; the subclass identity is preserved by Python's object model, not by the type hint.
- Eliminates
TypeVarboilerplate for chainable methods - Guarantees correct return types across inheritance hierarchies
- Fully supported by
mypy,pyright, and IDEs in Python 3.11+
The Problem with Legacy Self-Referencing Types
Legacy patterns relied on bound TypeVar declarations to approximate self-returning behavior. This approach introduces covariance pitfalls and requires redeclaring the TypeVar in every subclass that overrides a method. Static checkers struggle to resolve the exact subclass type when methods are chained across modules.
PEP 673 standardizes this behavior for static analysis. It removes the need for manual type variable scoping.
# For Python 3.10: from typing_extensions import Self
from typing import Self
# Legacy (Python <3.11)
# T = TypeVar("T", bound="QueryBuilder")
# class QueryBuilder:
# def where(self, condition: str) -> "T":
# return self # Static checkers flag: can't return Self as T
# Modern (Python 3.11+)
class QueryBuilder:
def where(self, condition: str) -> Self:
return self
Exact Syntax for Chainable Builder Methods
Each method explicitly declares Self as its return type, guaranteeing the exact instance type propagates through the chain. Do not annotate __init__ with Self — constructors implicitly return None.
The following implementation passes mypy --strict and pyright --strict without casting:
from typing import Self, Callable
class DataPipeline:
def __init__(self, data: list[int]) -> None:
self.data = data
def filter(self, threshold: int) -> Self:
self.data = [x for x in self.data if x > threshold]
return self
def transform(self, func: Callable[[int], int]) -> Self:
self.data = [func(x) for x in self.data]
return self
# Correctly inferred as DataPipeline
pipeline = DataPipeline([1, 2, 3, 4]).filter(2).transform(lambda x: x * 10)
CI-Ready Configuration:
# pyproject.toml
[tool.mypy]
strict = true
python_version = "3.11"
[tool.pyright]
typeCheckingMode = "strict"
pythonVersion = "3.11"
Inheritance and Subclass Return Type Safety
Self automatically propagates correct types to subclasses. You do not need to re-annotate methods or bind explicit TypeVar constraints in derived classes. Static checkers resolve the concrete subclass type at the call site, preventing base-class return type leakage during cross-module imports.
class AdvancedPipeline(DataPipeline):
def aggregate(self) -> Self:
self.data = [sum(self.data)]
return self
# Correctly inferred as AdvancedPipeline, not DataPipeline
result = AdvancedPipeline([1, 2, 3]).filter(1).transform(lambda x: x).aggregate()
Integrating with Protocols and Async Fluent APIs
Self works seamlessly in async def methods — the return annotation remains identical. Protocol compliance requires explicit Self declarations to enforce structural subtyping.
Checker Behavior Notes:
mypy: Fully supportsSelfin protocols and async methods under--strict.pyright: HandlesSelfnatively. Flags protocol mismatches aggressively.ruff: Enforces PEP 673 syntax consistency via theUPrule family but does not perform type inference.
from typing import Protocol, Self
import asyncio
class AsyncChainable(Protocol):
async def process(self) -> Self: ...
class StreamProcessor:
async def process(self) -> Self:
await asyncio.sleep(0)
return self
Note: StreamProcessor satisfies AsyncChainable structurally because it implements process with a compatible signature.
Common Mistakes
-
Using
Selfin__init__or__new__methodsSelfis strictly for methods returning the instance after initialization. Constructors implicitly returnNone; annotating them withSelftriggers static analysis errors. -
Mixing
Selfwith explicitTypeVarbounds in the same methodSelfreplaces the need forTypeVar("T", bound="Base"). Combining them creates conflicting type constraints that confuse static analyzers. -
Forgetting
typing_extensionsfallback for Python <3.11typing.Selfis available only from Python 3.11+. Projects targeting older versions must importSelffromtyping_extensionsto avoidImportError.
FAQ
Does typing.Self work with mypy strict mode?
Yes. mypy fully supports typing.Self in strict mode. It correctly validates return types, handles inheritance, and flags mismatched chainable method signatures.
How to handle typing.Self in Python 3.10 or lower?
Install typing_extensions and import Self from there. The runtime behavior is identical, and static checkers recognize the backport.
Can Self be used in class methods (@classmethod)?
Not as a bare annotation on self. For class methods returning an instance of the class, annotate the first parameter as cls: type[Self] and return cls(...). This correctly propagates the subclass type when the classmethod is called on a subclass.