Simplify Fluent Interfaces with Python typing.Self

Fluent interfaces rely heavily on method chaining, where methods return the current instance (self) so multiple calls can be linked in a single expression. Historically, accurately typing these methods in Python required cumbersome workarounds involving generic TypeVar definitions to ensure subclasses retained their specific types. Introduced in Python 3.11 via PEP 673, typing.Self directly solves this problem by providing a clean, intuitive type annotation that represents the enclosing class, automatically preserving type fidelity across class hierarchies.

The Problem with Earlier Approaches

Before typing.Self, annotating a method that returns the current instance presented a dilemma:

  1. Annotating with the class name: If you typed the return value using the class itself (e.g., def set_color(self) -> "Shape":), derived classes broke the type contract. When calling Circle().set_color(), a static type checker would infer the result as Shape rather than Circle, breaking autocomplete and type safety for subsequent method calls in the chain.

  2. Using generic TypeVar bindings: To fix inheritance, developers had to define an explicit generic type variable bound to the base class:

from typing import TypeVar

TShape = TypeVar("TShape", bound="Shape")


class Shape:

    def set_color(self: TShape, color: str) -> TShape:
        self.color = color
        return self

While functional, this approach introduced significant visual noise, required explicit typing on the self parameter, and added conceptual overhead to simple builder patterns.

How typing.Self Solves the Issue

The Self type annotation represents the dynamic type of self within the class body. When a subclass inherits a method annotated with Self, type checkers automatically resolve the return value to that specific subclass without requiring any generic boilerplate.

from typing import Self


class QueryBuilder:

    def __init__(self) -> None:
        self.query: list[str] = []

    def select(self, field: str) -> Self:
        self.query.append(f"SELECT {field}")
        return self

    def where(self, condition: str) -> Self:
        self.query.append(f"WHERE {condition}")
        return self


class AdvancedQueryBuilder(QueryBuilder):

    def paginate(self, limit: int, offset: int) -> Self:
        self.query.append(f"LIMIT {limit} OFFSET {offset}")
        return self

In this implementation, calling AdvancedQueryBuilder().select("name") accurately evaluates to AdvancedQueryBuilder instead of the base QueryBuilder. Consequently, IDEs and type checkers such as Mypy or Pyright can seamlessly chain inherited methods (select, where) with subclass-specific methods (paginate) without losing context.

Key Benefits