We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Type System as a Design Tool step 18 of 24
Self and the fluent query builder
Every ORM, HTTP client and query builder you have used depends on this, and gets it wrong at least once.
class QueryBuilder:
def where(self, clause: str) -> "QueryBuilder":
self._wheres.append(clause)
return self
That annotation is a lie the checker believes. At runtime where returns
self, which for a PagedQueryBuilder is a PagedQueryBuilder. Statically it
is a QueryBuilder, so the very first chained call downgrades the type, and
every subclass-specific method after it is a type error:
PagedQueryBuilder.from_table("t").where("x=1").page(2, 10)
# ^^^^ error: "QueryBuilder" has no attribute "page"
typing.Self (PEP 673, 3.11) fixes it in one word: -> Self means “the type of
self at the call site”, so the subclass’s type survives the whole chain.
The four places Self belongs
-
Fluent builders — every method that returns
self. -
Alternative constructors —
@classmethod def from_table(cls, ...) -> Selfreturningcls(...). Without it,PagedQueryBuilder.from_table("t")is statically aQueryBuilderand the chain is dead before it starts. -
__enter__— a context manager subclass otherwise loses its type inside thewithblock. -
Clone / copy methods —
def copy(self) -> Self.
And where it is wrong
Self is a promise. If a method always constructs the base class explicitly
— return QueryBuilder(self._table) rather than type(self)(...) or
cls(...) — then annotating it -> Self is a lie the checker cannot catch, and
a subclass will get an object of the wrong type with a static type that says
otherwise. Annotate that method with the concrete class it actually builds.
Self is also invalid in a @staticmethod (there is no self), at module
level, in a type alias, in a metaclass, and subscripted (Self[int]).
The task
class QueryBuilder:
@classmethod
def from_table(cls, table: str) -> Self: ...
def where(self, clause: str) -> Self: ...
def order_by(self, column: str) -> Self: ...
def limit(self, count: int) -> Self: ...
def build(self) -> str: ...
class PagedQueryBuilder(QueryBuilder):
def page(self, number: int, size: int) -> Self: ...
@override
def build(self) -> str: ...
QueryBuilder.build assembles, in this order and space-joined, omitting any
part that was never set:
SELECT * FROM <table> WHERE <c1> AND <c2> ORDER BY <col> LIMIT <n>
PagedQueryBuilder.page(number, size) records a 1-based page, clears any
limit set earlier, and its build appends LIMIT <size> OFFSET <(number-1)*size>
to whatever the base class produced.
solve(table, wheres, order, limit, page, per_page) builds two queries and
returns both: a plain QueryBuilder chain (applying order_by when order is
non-empty and limit when limit > 0), and a PagedQueryBuilder chain
(applying page when page > 0).
The line that proves it
paged = PagedQueryBuilder.from_table(table)
for clause in wheres:
paged = paged.where(clause)
...
paged = paged.page(page, per_page)
paged must still be statically a PagedQueryBuilder after from_table and
after every where. If any of those methods returns QueryBuilder, the final
line does not type-check — which is the entire assertion, and the reason your
solution will not pass the gate without Self.
@override on PagedQueryBuilder.build is the other half of the same
discipline: it makes the checker verify that a method by that name really does
exist on the base. Decorator ordering matters — @override goes innermost,
closest to the def.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.