Python is a dynamic language, but adding type hints lets editors and static analyzers catch more mistakes early. This lesson walks through the essential features of the typing module and shows how to design interfaces using Protocol. Do not try to memorize every term at once—proceed in this order: “leave expectations on function parameters,” “document dictionary shapes,” then “describe interfaces by behavior instead of names.”
Key terms
- TypedDict: a tool that defines dictionary keys and value types like a lightweight class
- Static analyzer: tools such as
mypyorpyrightthat read hints without running your code and report type issues
Core ideas
Study notes
- Time required: 70–80 minutes (core sections)
- Prerequisites: function/class design, exception handling, a pytest-ready codebase
- Goal: annotate critical functions, build a Protocol-based interface, and run a static check
- Type hints document the types a function or variable expects.
- Protocols are structural interfaces that describe duck typing statically.
- TypedDict documents the structure of dictionaries.
- Static analyzers such as mypy and pyright read hints and flag errors.
- Finish the Core sections first; “optional extension” parts can wait.
Code examples
Type-hint fundamentals (Core)
def grade(score: int) -> str:
if score >= 90:
return "A"
if score >= 80:
return "B"
return "C"
score: intannotates a parameter.-> strdocuments the return type.
The interpreter does not enforce hints, but tools like mypy and pyright do.
Annotate CLI parameters (Core)
Think about a Typer command handler: if you confuse strings for numbers or forget to handle None, the CLI stops immediately.
from mealbot.notifier import SlackNotifier
def send_meal(school_code: str, *, limit: int | None = None) -> list[str]:
"""Return a list of messages for the given school."""
data = fetch_meal(school_code)
menu: list[str] = data["menu"]
if limit is not None:
menu = menu[:limit]
return menu
def push_to_slack(menu: list[str], notifier: SlackNotifier) -> None:
for line in menu:
notifier.send(line)
- The signature alone tells you what goes in and out.
limit: int | Nonemakes it obvious the caller must handle the optional case.- Specific types like
list[str]feed editor auto-completion.
Collections and optional types (Core)
from typing import List, Dict, Optional
users: List[str] = ["민지", "준호"]
scores: Dict[str, int] = {"민지": 92}
maybe_score: Optional[int] = scores.get("지훈")
Optional[int]means “eitherintorNone.”- On Python 3.9+, you can write
list[str]anddict[str, int]directly.
Type aliases and TypedDict (Optional)
from typing import TypedDict
class User(TypedDict):
name: str
is_active: bool
def deactivate(user: User) -> User:
user["is_active"] = False
return user
TypedDict locks in the dictionary structure and documents the keys.
Introducing Protocol (Core → Plus)
typing.Protocol describes duck typing as an abstract interface. Any class that matches the method signatures is considered compatible, regardless of inheritance.
from typing import Protocol
class Notifier(Protocol):
def send(self, message: str) -> None:
...
def broadcast(notifier: Notifier, payload: str) -> None:
notifier.send(payload)
class SlackNotifier:
def send(self, message: str) -> None:
print(f"slack: {message}")
class SMSNotifier:
def send(self, message: str) -> None:
print(f"sms: {message}")
Even though SlackNotifier and SMSNotifier never inherit from Notifier, static analyzers accept them because they implement send. In other words, you declare “what an object can do,” not “what it is named.” Test doubles from the CLI exercise only need to match the same signature.
Structural subtyping and runtime_checkable (Optional)
from typing import runtime_checkable
@runtime_checkable
class Exporter(Protocol):
def export(self, data: dict) -> bytes:
...
def run_export(obj: Exporter):
assert isinstance(obj, Exporter)
return obj.export({})
Adding @runtime_checkable allows isinstance checks, though it can only verify method presence. For complex constraints, consider abc.ABC.
Integrate the type checker (Core → Plus)
If you have never run a static check, aim for a single command such as uv run mypy src. Later you can wire it into CI or your IDE.
- Install with
uv add mypyand runmypy src. - VS Code and PyCharm can lint on save.
- In CI, raise the bar with
mypy --strict.
direction: right
editor: "VS Code/PyCharm
write hints"
checker: "mypy/pyright
static analysis"
ci: "CI pipeline
`uv run mypy`"
runtime: "Runtime
pytest/execute"
editor -> checker: "lint on save"
checker -> ci: "share rules"
ci -> runtime: "deploy checked code"
Connecting your editor, static checker, and CI keeps everyone on the same contract.
Why it matters
- Type hints are optional but dramatically improve collaboration.
- Protocols let you define interfaces by behavior.
- Wiring a static checker into CI keeps the safety net active.
Practice
- Follow along: Annotate
gradeandbroadcast, then runmypyto confirm they pass. - Extend: Define a Protocol, swap Slack/SMS implementations, and experiment with
TypedDictorruntime_checkable. - Debug: Deliberately assign a wrong return type, observe the
mypyerror, then fix it and note the difference. - Definition of done: Core functions and Protocols type-check cleanly, and optional sections are logged as future reading.
Wrap-up
Next, dive into Python's async/await syntax to write asynchronous code.
💬 댓글
이 글에 대한 의견을 남겨주세요