Search Tech Journey

Find topics, journeys and posts

back to blog
engineeringadvanced 12m2026-06-18

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 (catches swap(user_id, order_id)).
  • ParamSpec / Concatenate: type-preserving decorators.
  • Self: builders that return self.

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 / cache for memoisation.
  • functools.singledispatch for type-based dispatch.
  • itertools for streams (groupby, accumulate, chain).
  • more_itertools for the rest.
  • operator.attrgetter / itemgetter for 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 manual gather for proper cancel propagation.
  • async with for resource cleanup.
  • aiohttp / httpx for 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)

PythonC#
@dataclassrecord
Protocolinterface (nominal)
matchswitch expression with patterns
Optional[X]X? nullable refs
asyncioTask / async/await
withusing

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=[])) → use None sentinel.
  • Mixing dict.get(k, default) with if 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).
  • parametrize for table-driven tests.
  • monkeypatch for clean injection.
  • tmp_path for filesystem tests.
  • freezegun for 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.

Key points

    Resources

    Practice Problem: Validate Binary Search Tree (Medium)