Skip to content

Commit 21e4add

Browse files
committed
initial
0 parents  commit 21e4add

1 file changed

Lines changed: 391 additions & 0 deletions

File tree

slides.md

Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
1+
---
2+
# You can also start simply with 'default'
3+
theme: default
4+
# some information about your slides (markdown enabled)
5+
title: Welcome to Slidev
6+
info: |
7+
## Slidev Starter Template
8+
Presentation slides for developers.
9+
Learn more at [Sli.dev](https://sli.dev)
10+
# apply unocss classes to the current slide
11+
class: text-center
12+
# https://sli.dev/features/drawing
13+
drawings:
14+
persist: false
15+
# slide transition: https://sli.dev/guide/animations.html#slide-transitions
16+
transition: slide-left
17+
# enable MDC Syntax: https://sli.dev/features/mdc
18+
mdc: true
19+
20+
---
21+
22+
# Getting Past "It Runs, So It’s Fine"
23+
24+
## Eight Common Python 3.12 Typing Mistakes (and How to Fix Them)
25+
26+
Django 5.1 backend engineering guild meeting
27+
28+
---
29+
30+
# Mistake 1 ‑‑ Unintentional Any Propagation
31+
32+
```python
33+
# utils/db.py
34+
35+
from django.db import connection
36+
37+
def run_sql(sql: str, params: list) -> list:
38+
with connection.cursor() as cur:
39+
cur.execute(sql, params)
40+
return cur.fetchall() # ← mypy infers "list[Any]"
41+
```
42+
43+
---
44+
45+
# What happened?
46+
47+
- fetchall() comes from an untyped stub, so its return type defaults to Any.
48+
- That single Any silently spreads everywhere the result travels.
49+
50+
## Solution
51+
52+
- Add a return type that describes the rows – e.g. `list[tuple[int, str]]`.
53+
- Pin a stub package (e.g. types-Django) or declare a TypedDict/Protocol for rows.
54+
- Compile with --strict (mypy) or --warn‑unused‑ignores (pyright) to surface the leak.
55+
56+
```python
57+
from typing import TypeAlias
58+
59+
Row: TypeAlias = tuple[int, str] # or a TypedDict when column‑named
60+
61+
def run_sql(sql: str, params: list[object]) -> list[Row]:
62+
pass
63+
```
64+
65+
---
66+
67+
## zoom: 1.5
68+
69+
# Mistake 2 ‑‑ Overusing typing.cast
70+
71+
```python
72+
# services/payments.py
73+
74+
from typing import cast
75+
from decimal import Decimal
76+
77+
def as_dollars(amount: str | Decimal) -> Decimal:
78+
return cast(Decimal, amount) \* Decimal("0.01")
79+
```
80+
81+
---
82+
83+
## zoom: 1.2
84+
85+
# What’s wrong?
86+
87+
- cast() lies to the type checker – it asserts the value is already a Decimal without runtime verification.
88+
- If a str sneaks in, you get a TypeError.
89+
90+
## Better pattern
91+
92+
```python
93+
def as_dollars(amount: str | Decimal) -> Decimal:
94+
if isinstance(amount, Decimal):
95+
return amount * Decimal("0.01")
96+
try:
97+
return Decimal(amount) * Decimal("0.01")
98+
except Exception as exc: # validation, not blind casting
99+
raise ValueError("Bad amount") from exc
100+
```
101+
102+
---
103+
104+
zoom: 1.4
105+
layout: center
106+
107+
---
108+
109+
# Mistake 3 ‑‑ Bare Containers (missing generics)
110+
111+
```python
112+
def load_ids() -> list:
113+
with open("ids.txt") as fh:
114+
return [int(line) for line in fh]
115+
```
116+
117+
---
118+
119+
# Why it matters
120+
121+
`list` without `[...]` returns `list[Any]`; later code gets no help.
122+
123+
## Fix
124+
125+
```python
126+
def load_ids() -> list[int]:
127+
...
128+
```
129+
130+
- Turn on `warn_bare_types` = true (mypy) or reportImplicitAny = true (pyright).
131+
132+
---
133+
134+
# Mistake 5 ‑‑ Mutable Default Values + Typing Confusion
135+
136+
```python
137+
from dataclasses import dataclass
138+
139+
@dataclass
140+
class Accumulator:
141+
seen: list[str] = []
142+
```
143+
144+
---
145+
146+
# What's wrong?
147+
148+
- Runtime bug—all instances share one list.
149+
- Typing confusion—default `[]` is fine syntactically but masks the shared‑state issue.
150+
151+
```python
152+
from dataclasses import dataclass, field
153+
154+
@dataclass
155+
class Accumulator:
156+
seen: list[str] = field(default_factory=list)
157+
```
158+
159+
- Linters like ruff‑b013 or mypy’s --strict-equality flag prevent this.
160+
161+
---
162+
163+
# Mistake 6 ‑‑ Ignoring Self for Fluent APIs
164+
165+
```python
166+
class QueryBuilder:
167+
def filter(self, **kw) -> "QueryBuilder":
168+
...
169+
return self
170+
171+
def eager(self) -> "QueryBuilder":
172+
...
173+
return self
174+
```
175+
176+
---
177+
178+
# What's wrong?
179+
180+
- Using literal string annotations works, but Python 3.12 offers `typing.Self` – clearer & future‑proof.
181+
182+
```python
183+
from typing import Self
184+
185+
class QueryBuilder:
186+
def filter(self, **kw) -> Self:
187+
...
188+
return self
189+
190+
def eager(self) -> Self:
191+
...
192+
return self
193+
```
194+
195+
- Now subclasses inherit the correct return type automatically.
196+
197+
---
198+
199+
# Mistake 7 ‑‑ Untyped Django QuerySets
200+
201+
```python
202+
users = User.objects.filter(is_active=True) # inferred as QuerySet[Any]
203+
204+
def first_email() -> str:
205+
return users[0].email
206+
```
207+
208+
---
209+
210+
# How to fix?
211+
212+
- Install django-stubs or types-Django so .objects returns `QuerySet[User]`.
213+
- Or annotate yourself:
214+
215+
```python
216+
from django.db.models import QuerySet
217+
218+
users: QuerySet[User] = User.objects.filter(is_active=True)
219+
220+
def first_email() -> str:
221+
return users[0].email
222+
```
223+
224+
---
225+
226+
# Mistake 8 ‑‑ Over‑wide Unions Instead of Protocols
227+
228+
```python
229+
def to_jsonable(obj: str | int | float | Decimal) -> str | int | float:
230+
if isinstance(obj, Decimal):
231+
return float(obj)
232+
return obj
233+
```
234+
235+
---
236+
237+
# What's wrong?
238+
239+
- API really wants "anything that can become an int" or "has **float**" → use a `Protocol`.
240+
241+
```python
242+
from typing import Protocol, runtime_checkable
243+
244+
@runtime_checkable
245+
class SupportsJSON(Protocol):
246+
def __float__(self) -> float: ...
247+
248+
def to_jsonable(obj: str | int | SupportsJSON) -> str | int | float:
249+
if isinstance(obj, SupportsJSON):
250+
return float(obj)
251+
return obj
252+
```
253+
254+
- Reduces union sprawl and tightens guarantees.
255+
256+
---
257+
258+
## zoom: 1.4
259+
260+
# Mistake 9 — Confusing Any with Unknown
261+
262+
```python
263+
from typing import Any
264+
import json, pathlib
265+
266+
def load_conf(path: pathlib.Path | str) -> Any:
267+
with open(path) as fh:
268+
return json.load(fh) # 👈 returns "Any"
269+
```
270+
271+
---
272+
273+
# Key difference
274+
275+
<table>
276+
<tr><th>Any</th><th>Unknown</th></tr>
277+
<tr><td>Opt-out: all operations are allowed; errors are suppressed</td><td>Opt-in: no operation is allowed until the value is narrowed or cast.</td></tr>
278+
<tr><td>Spreads silently, hiding type holes.</td><td>Shines a spotlight on every place you forgot a real type.</td></tr>
279+
</table>
280+
281+
Pyright defaults to Unknown when inference fails - exactly to expose "blind spots." 
282+
283+
---
284+
285+
## Better pattern
286+
287+
```python
288+
def load_conf(path: Path | str) -> Unknown: # ← explicit
289+
data = json.loads(Path(path).read_text())
290+
291+
# Validate/narrow before use
292+
if not isinstance(data, dict) or "version" not in data:
293+
raise ValueError("bad config format")
294+
295+
assert_type(data, dict[str, Unknown]) # editor helper
296+
return data
297+
```
298+
299+
## Practical tips
300+
301+
- Run Pyright in --strict mode so implicit Any becomes Unknown.
302+
- Keep typed-stub deps current (e.g. pip install --upgrade django-stubs) so external libraries don’t leak Any.
303+
304+
---
305+
306+
# Mistake 10 — Relying on hasattr / getattr Without Telling the Type Checker
307+
308+
```python
309+
def tally(obj, count: int) -> int:
310+
if hasattr(obj, "total"): # duck-typing at runtime
311+
return obj.total + count # 🔴 pyright: "obj" still Any
312+
raise TypeError("object missing total")
313+
```
314+
315+
---
316+
317+
# Why it backfires
318+
319+
- 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.
320+
- Two robust options
321+
322+
## 1. Structural Protocol
323+
324+
```python
325+
from typing import Protocol
326+
327+
class HasTotal(Protocol):
328+
total: int
329+
330+
def tally(obj: HasTotal, count: int) -> int:
331+
return obj.total + count
332+
```
333+
334+
Any object with an int .total now passes, and misuse is caught at call-site.
335+
336+
---
337+
338+
## 2. Custom TypeGuard
339+
340+
```python
341+
from typing import TypeGuard
342+
343+
def has_total(x: object) -> TypeGuard["HasTotal"]:
344+
return hasattr(x, "total") and isinstance(getattr(x, "total"), int)
345+
346+
def tally(obj: object, count: int) -> int:
347+
if has_total(obj): # type narrows here ✔
348+
return obj.total + count
349+
raise TypeError("object missing total")
350+
```
351+
352+
- TypeGuard communicates the narrowing contract directly to the checker.
353+
354+
## Take-away
355+
356+
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.
357+
358+
---
359+
360+
# Workflow Trick - Using `reveal_type` for Instant Feedback
361+
362+
```python
363+
# debugging_types.py
364+
from typing import assert_type, reveal_type
365+
366+
def maybe_dict(flag: bool):
367+
if flag:
368+
data = {"key": 1}
369+
else:
370+
data = ["fallback"]
371+
372+
reveal_type(data)
373+
assert_type(data, dict[str, int] | list[str])
374+
return data
375+
```
376+
377+
- Sanity check while spiking code or refactoring
378+
- Immediately surfaces surprise `Any`/`Unknown` leaks
379+
- Combine with `assert_type()` to lock in expectations and catch regressions in CI.
380+
381+
---
382+
383+
# Key take‑aways
384+
385+
1. Compile in strict mode; don’t patch the holes later.
386+
2. Treat `Any` and unchecked `cast()` like run‑time `eval()`—they break guarantees.
387+
3. Prefer precise, minimal types over “works for everything” unions.
388+
4. Lean on newer features (Self, `assert_never`, TypeAlias, TypeVarTuple, …).
389+
5. Keep stubs up to date: django-stubs, types‑requests, etc.
390+
6. Validate at boundaries; trust types inside the boundary.
391+

0 commit comments

Comments
 (0)