Day 20 — Idiomatic Python (and C#) — Type Hints, Protocols, Dataclasses, Pattern Matching
Idiomatic code is the difference between a senior who writes maintainable systems and a junior who writes 'Python that runs'. Type hints + Protocols + dataclass…
Modern Python (3.12+) gives you almost all the static-typing comfort of C# while keeping the dynamic flexibility. The trick is knowing which tools to reach for and when.
🧠 Concept
Why it matters & the mental model.
1. Type hints — not just for the IDE
Catch errors before runtime, document intent, enable refactoring. Use from __future__ import annotations (PEP 563) to keep annotations as strings → no runtime overhead. Run mypy or pyright in CI. pyright is faster, stricter, ships with Pylance.
2. The killer types
Optional[X]/X | None: nullable.Union[A, B]/A | B: discriminated.Literal["red", "green", "blue"]: enum-lite without a class.TypedDict: typed dicts (great for JSON shapes).NewType("UserId", int): nominal typing for IDs (catchesswap(user_id, order_id)).ParamSpec/Concatenate: type-preserving decorators.Self: builders that returnself.
3. Protocols — structural typing
from typing import Protocol
class Reader(Protocol):
def read(self, n: int) -> bytes: ...
def consume(r: Reader) -> None: ...
Any object with .read(n) -> bytes satisfies Reader — no inheritance. Better than ABCs because third-party objects can fit your interface retroactively.
4. Dataclasses & friends
@dataclass(slots=True, frozen=True)
class Point:
x: float
y: float
def translate(self, dx: float, dy: float) -> "Point":
return replace(self, x=self.x + dx, y=self.y + dy)
slots=True → faster attribute access, lower memory. frozen=True → immutable, hashable, safe in sets. For runtime validation: pydantic BaseModel or attrs with validators.
🛠 Deep Dive
Internals, code, architecture.
5. Pattern matching (PEP 634)
match event:
case {"type": "click", "x": x, "y": y}:
...
case {"type": "key", "key": k} if k.isalpha():
...
case _:
...
Great for event dispatch, AST walking, parser combinators.
6. Functional bits
functools.lru_cache/cachefor memoisation.functools.singledispatchfor type-based dispatch.itertoolsfor streams (groupby,accumulate,chain).more_itertoolsfor the rest.operator.attrgetter/itemgetterfor cleaner lambdas.
7. Errors as values
Python is exception-first, but for control flow consider Result[T, E] types (e.g. returns library) when failure is expected (parsing, network) — makes the type signature honest.
8. Async idioms
asyncio.TaskGroup(3.11+) over manualgatherfor proper cancel propagation.async withfor resource cleanup.aiohttp/httpxfor async HTTP.- Avoid blocking calls in async code; use
asyncio.to_thread.
🚀 In Practice
Trade-offs, exercises, what to ship today.
9. C# parallels (when you context-switch)
| Python | C# |
|---|---|
@dataclass | record |
Protocol | interface (nominal) |
match | switch expression with patterns |
Optional[X] | X? nullable refs |
asyncio | Task / async/await |
with | using |
C# is more strictly nominal — record + interface + nullable reference types give similar guarantees with stronger guarantees from the compiler. LINQ ≈ Python's itertools + generator expressions.
10. Anti-patterns
- Mutable default args (
def f(x=[])) → useNonesentinel. - Mixing
dict.get(k, default)withif k in d:— pick one style. - Stringly-typed everything — prefer Enums / Literals.
- Skipping
__hash__/__eq__on value objects — broken sets/dicts. - Using inheritance for code reuse — prefer composition.
11. Testing idioms
- Fixtures over setUp/tearDown (pytest).
parametrizefor table-driven tests.monkeypatchfor clean injection.tmp_pathfor filesystem tests.freezegunfor time-dependent code.
12. What to take away
Reading a candidate's code: type hints + dataclasses + Protocols → senior. from typing import * + bare Any everywhere → not. Pattern matching shows up-to-date Python knowledge.
Resources
- 🎥 ArjanCodes — Modern Python type hints
- 📖 Python typing docs (3.13)
- 📖 PEP 544 — Protocols (structural subtyping)
- 📖 C# in depth — Jon Skeet (chapter excerpts)
Practice Problem: Validate Binary Search Tree (Medium)