Skip to content

← Interfaces with Protocols and ABCs step 16 of 26

Medium Framework

Restrict One Method to String Builders with Self Types

In the type hints track, you gave a fluent query builder Self return types. A chain of calls on a subclass then kept the subclass's own methods. This exercise adds two things. The builder's methods return a new builder instead of changing the old one, and one method works only on builders of strings.

QueryBuilder[T] is generic, and T records what kind of value the query is about. The starter annotates each method as returning "QueryBuilder[T]", so a chain on an AuditedQueryBuilder loses its audit() method. Submit the starter, and mypy reports:

L35:9 [attr-defined]
"QueryBuilder[str]" has no attribute "audit"

Return Self from a method that builds a new object

Self stands for the class of the object you call the method on. The method does not have to return self itself. where builds a new builder with type(self)(...), which has the same class as self, so Self fits. The same goes for the class method from_table, which calls cls(table).

Allow a method on string builders only

LIKE matches text, so like should type-check only on a QueryBuilder[str]. Give like its own type parameter, and annotate self with it:

def like[S: QueryBuilder[str]](self: S, pattern: str) -> S:

S: QueryBuilder[str] means S can be QueryBuilder[str] or any subclass of it. On a QueryBuilder[int], mypy rejects the call with Invalid self argument "QueryBuilder[int]" to attribute function "like". Returning S keeps the class of self, so audit() still works after like on an AuditedQueryBuilder[str]. At runtime every builder still has a like method. Only mypy rejects the call.

What to write

Import Self from typing, and change only these annotations:

  • from_table, where and limit return Self.
  • like has the signature above.

Keep the method bodies, the constructor, AuditedQueryBuilder and solve as they are. Do not use Any or cast to get past mypy.

The bodies already do the right thing. Each method returns a new builder of the same class with one more clause, and leaves the old builder unchanged. build() joins the clauses into one string with no escaping, so never use it on input you do not trust.

What the driver returns

solve(table, pattern, limit) builds one chain on a string builder, one on an integer builder and one on an audited string builder. For table="users", pattern="pat" and limit=5, it returns:

[
    "SELECT * FROM users WHERE a = 1 LIKE 'pat' LIMIT 5",
    "SELECT * FROM users WHERE id = 2 LIMIT 5",
    "AUDIT[SELECT * FROM users WHERE b = 3 LIKE 'pat']",
]

The starter already returns these strings, so mypy is what this exercise grades. The third chain type-checks only when both where and like keep the subclass.