| theme | default | |
|---|---|---|
| title | Welcome to Slidev | |
| info | ## Slidev Starter Template Presentation slides for developers. Learn more at [Sli.dev](https://sli.dev) | |
| class | text-center | |
| drawings |
|
|
| transition | slide-left | |
| mdc | true |
Django 5.1 backend engineering guild meeting
# utils/db.py
from django.db import connection
def run_sql(sql: str, params: list) -> list:
with connection.cursor() as cur:
cur.execute(sql, params)
return cur.fetchall() # ← mypy infers "list[Any]"- fetchall() comes from an untyped stub, so its return type defaults to Any.
- That single Any silently spreads everywhere the result travels.
- Add a return type that describes the rows – e.g.
list[tuple[int, str]]. - Pin a stub package (e.g. types-Django) or declare a TypedDict/Protocol for rows.
- Compile with --strict (mypy) or --warn‑unused‑ignores (pyright) to surface the leak.
from typing import TypeAlias
Row: TypeAlias = tuple[int, str] # or a TypedDict when column‑named
def run_sql(sql: str, params: list[object]) -> list[Row]:
pass# services/payments.py
from typing import cast
from decimal import Decimal
def as_dollars(amount: str | Decimal) -> Decimal:
return cast(Decimal, amount) \* Decimal("0.01")- cast() lies to the type checker – it asserts the value is already a Decimal without runtime verification.
- If a str sneaks in, you get a TypeError.
def as_dollars(amount: str | Decimal) -> Decimal:
if isinstance(amount, Decimal):
return amount * Decimal("0.01")
try:
return Decimal(amount) * Decimal("0.01")
except Exception as exc: # validation, not blind casting
raise ValueError("Bad amount") from exczoom: 1.4 layout: center
def load_ids() -> list:
with open("ids.txt") as fh:
return [int(line) for line in fh]– list without [...] returns list[Any]; later code gets no help.
def load_ids() -> list[int]:
...- Turn on
warn_bare_types= true (mypy) or reportImplicitAny = true (pyright).
from dataclasses import dataclass
@dataclass
class Accumulator:
seen: list[str] = []- Runtime bug—all instances share one list.
- Typing confusion—default
[]is fine syntactically but masks the shared‑state issue.
from dataclasses import dataclass, field
@dataclass
class Accumulator:
seen: list[str] = field(default_factory=list)- Linters like ruff‑b013 or mypy’s --strict-equality flag prevent this.
class QueryBuilder:
def filter(self, **kw) -> "QueryBuilder":
...
return self
def eager(self) -> "QueryBuilder":
...
return self- Using literal string annotations works, but Python 3.12 offers
typing.Self– clearer & future‑proof.
from typing import Self
class QueryBuilder:
def filter(self, **kw) -> Self:
...
return self
def eager(self) -> Self:
...
return self- Now subclasses inherit the correct return type automatically.
users = User.objects.filter(is_active=True) # inferred as QuerySet[Any]
def first_email() -> str:
return users[0].email- Install django-stubs or types-Django so .objects returns
QuerySet[User]. - Or annotate yourself:
from django.db.models import QuerySet
users: QuerySet[User] = User.objects.filter(is_active=True)
def first_email() -> str:
return users[0].emaildef to_jsonable(obj: str | int | float | Decimal) -> str | int | float:
if isinstance(obj, Decimal):
return float(obj)
return obj- API really wants "anything that can become an int" or "has float" → use a
Protocol.
from typing import Protocol, runtime_checkable
@runtime_checkable
class SupportsJSON(Protocol):
def __float__(self) -> float: ...
def to_jsonable(obj: str | int | SupportsJSON) -> str | int | float:
if isinstance(obj, SupportsJSON):
return float(obj)
return obj- Reduces union sprawl and tightens guarantees.
from typing import Any
import json, pathlib
def load_conf(path: pathlib.Path | str) -> Any:
with open(path) as fh:
return json.load(fh) # 👈 returns "Any"| Any | Unknown |
|---|---|
| Opt-out: all operations are allowed; errors are suppressed | Opt-in: no operation is allowed until the value is narrowed or cast. |
| Spreads silently, hiding type holes. | Shines a spotlight on every place you forgot a real type. |
Pyright defaults to Unknown when inference fails - exactly to expose "blind spots." 
def load_conf(path: Path | str) -> Unknown: # ← explicit
data = json.loads(Path(path).read_text())
# Validate/narrow before use
if not isinstance(data, dict) or "version" not in data:
raise ValueError("bad config format")
assert_type(data, dict[str, Unknown]) # editor helper
return data- Run Pyright in --strict mode so implicit Any becomes Unknown.
- Keep typed-stub deps current (e.g. pip install --upgrade django-stubs) so external libraries don’t leak Any.
def tally(obj, count: int) -> int:
if hasattr(obj, "total"): # duck-typing at runtime
return obj.total + count # 🔴 pyright: "obj" still Any
raise TypeError("object missing total")- The runtime hasattr check does ensure the attribute exists, but the type checker can’t see that guarantee—obj stays
Any/Unknown, so no help or safety. - Two robust options
from typing import Protocol
class HasTotal(Protocol):
total: int
def tally(obj: HasTotal, count: int) -> int:
return obj.total + countAny object with an int .total now passes, and misuse is caught at call-site.
from typing import TypeGuard
def has_total(x: object) -> TypeGuard["HasTotal"]:
return hasattr(x, "total") and isinstance(getattr(x, "total"), int)
def tally(obj: object, count: int) -> int:
if has_total(obj): # type narrows here ✔
return obj.total + count
raise TypeError("object missing total")- TypeGuard communicates the narrowing contract directly to the checker.
Whenever you branch on attribute presence or value, express that promise to the type system—either with a Protocol for cheap "duck typing" or a TypeGuard when the assertion is non-trivial.
# debugging_types.py
from typing import assert_type, reveal_type
def maybe_dict(flag: bool):
if flag:
data = {"key": 1}
else:
data = ["fallback"]
reveal_type(data)
assert_type(data, dict[str, int] | list[str])
return data- Sanity check while spiking code or refactoring
- Immediately surfaces surprise
Any/Unknownleaks - Combine with
assert_type()to lock in expectations and catch regressions in CI.
- Compile in strict mode; don’t patch the holes later.
- Treat
Anyand uncheckedcast()like run‑timeeval()—they break guarantees. - Prefer precise, minimal types over “works for everything” unions.
- Lean on newer features (Self,
assert_never, TypeAlias, TypeVarTuple, …). - Keep stubs up to date: django-stubs, types‑requests, etc.
- Validate at boundaries; trust types inside the boundary.