Python typing.Self: Fluent Return Types

Published on: August 28, 2026
Reading time: 5 minutes
Close-up view of a computer screen displaying code in a software development environment.

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 SqlQuery

The 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 self

A 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.5

This 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 current

This 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 self

For 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 self

Self 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 new

The 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 Self

Libraries 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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    A conceptual image showing error code projected on binary data with smoke.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ExceptionGroup: Multiple Errors

    Learn Python ExceptionGroup for multiple errors, except*, nested groups, TaskGroup, filtering, logging, API compatibility, and batch validation.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Path.walk: Traverse Directories

    Learn Python Path.walk to traverse directories, prune folders, handle errors and symlinks, calculate sizes, delete safely, and support older versions.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python asyncio.timeout: Control Deadlines

    Learn Python asyncio.timeout for deadlines, timeout_at, rescheduling, TaskGroup, cleanup, retries, shielding, and safe asynchronous cancellation.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    A detailed view of computer programming code on a screen, showcasing software development.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python TaskGroup: Structured Concurrency

    Learn Python TaskGroup for structured concurrency, task results, cancellation, ExceptionGroup, timeouts, nested groups, and bounded async work.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    Close-up view of a computer screen displaying code in a software development environment.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python MappingProxyType: Read-Only Dict

    Learn Python MappingProxyType to expose read-only dictionaries, create dynamic views and snapshots, preserve invariants, and avoid unnecessary copies.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026
    Close-up of a metal gate featuring a 'Please Keep Gate Closed' sign, outdoors.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Literal: Restrict Exact Values

    Learn Python Literal to restrict exact values, create overloads, discriminate TypedDict variants, use match/case, and improve typed APIs.

    Ler mais

    Tempo de leitura: 4 minutos
    28/08/2026