Course progress Course outline 24 of 24 lessons available
Python Foundations
Data and Collections
Building Reliable Programs
Modeling with Objects
Professional Python
Advanced Python
Type hints are sticky notes, not guards
A type hint tells readers and static tools what a value should be. It does not normally stand at the door and stop a different runtime value.
-
Annotation
name: str - Type checker reads before running
- Python uses the actual object
def repeat(label: str, times: int) -> str:
return label * times
print(repeat("go", 3))
print(repeat(2, 3))
gogogo
6
A checker should flag the second call, yet Python performs integer multiplication. Validate data from users, files, and networks yourself. Annotations provide no automatic conversion either.
Describe boxes and maybes
Collection hints describe both the container and its contents:
def total_by_topic(sessions: list[tuple[str, int]]) -> dict[str, int]:
totals: dict[str, int] = {}
for topic, minutes in sessions:
totals[topic] = totals.get(topic, 0) + minutes
return totals
print(total_by_topic([("Python", 20), ("Python", 25)]))
{'Python': 45}
The hint does not inspect every item at runtime. Ask only for capabilities you need: a read-only function that merely loops can accept Iterable[str]; one that indexes can accept Sequence[str]; a function that mutates specifically may need list[str].
str | None means a string or None. It is the modern spelling of Optional[str]. int | str expresses the same union idea as Union[int, str].
def author_label(author: str | None) -> str:
if author is None:
return "Unknown"
return author.strip().title()
After the None branch returns, a checker can narrow author to str. Optional does not mean omittable: calling author_label() still needs an argument. Use author: str | None = None when omission is allowed.
-
Union
int | str -
Check
isinstance(value, int) - Int branch integer operations are safe
- Else branch string operations are safe
A type alias gives a repeated shape a domain name:
from typing import TypeAlias
Session: TypeAlias = tuple[str, int]
Python 3.12 also supports type Session = tuple[str, int]. An alias is vocabulary, not a new runtime class or validator. Use a dataclass when fields need names or rules.
Describe behavior and preserve relationships
Callable[[str], str] describes something callable with one string that returns a string:
from collections.abc import Callable
def clean_all(words: list[str], clean: Callable[[str], str]) -> list[str]:
return [clean(word) for word in words]
It can accept a function, bound method, or callable object with that contract.
A TypeVar keeps a relationship instead of naming one concrete type:
from collections.abc import Sequence
from typing import TypeVar
T = TypeVar("T")
def first_or_none(items: Sequence[T]) -> T | None:
return items[0] if items else None
For list[int], the result is int | None; for list[str], it is str | None. Returning object | None would lose that useful connection. Add a type variable only when it expresses a real input-output relationship.
A Protocol is a capability card
A Protocol names the small behavior a consumer needs. Classes satisfy it structurally by having compatible members; they do not need to inherit from it.
-
Protocol
requires
summary() - Article has that capability
- Video has it too
from typing import Protocol
class Summarizable(Protocol):
def summary(self) -> str:
...
def show(item: Summarizable) -> None:
print(item.summary())
Keep protocols small. @runtime_checkable permits a limited isinstance() presence check, but it does not validate full method signatures or annotated value types:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Fetcher(Protocol):
def fetch(self) -> list[str]:
...
class WrongShape:
def fetch(self, required_argument):
return 42
print(isinstance(WrongShape(), Fetcher))
This can print True because fetch exists. Static checking and runtime data validation still matter.
Tiny project: Typed Notification Digest
Create notification_digest.py. from __future__ import annotations lets this modern notation run on older supported interpreters too. The project combines a type alias, callable, generic helper, protocol, and real runtime validation.
from __future__ import annotations
from collections.abc import Callable, Iterable, Sequence
from dataclasses import dataclass
from typing import Protocol, TypeVar
RawNotice = tuple[str, int]
T = TypeVar("T")
@dataclass(frozen=True)
class Notice:
source: str
message: str
score: int
def __post_init__(self) -> None:
if not self.source.strip() or not self.message.strip():
raise ValueError("text must not be blank")
if isinstance(self.score, bool) or not isinstance(self.score, int):
raise TypeError("score must be an integer")
if not 0 <= self.score <= 100:
raise ValueError("score must be from 0 to 100")
class NoticeSource(Protocol):
def fetch(self) -> Iterable[Notice]:
...
class MemorySource:
def __init__(self, name: str, records: list[RawNotice]) -> None:
self.name = name
self.records = records
def fetch(self) -> Iterable[Notice]:
for message, score in self.records:
yield Notice(self.name, message, score)
def first_or_none(items: Sequence[T]) -> T | None:
return items[0] if items else None
def collect(
sources: Iterable[NoticeSource], keep: Callable[[Notice], bool]
) -> list[Notice]:
notices = [
notice
for source in sources
for notice in source.fetch()
if keep(notice)
]
return sorted(notices, key=lambda notice: (-notice.score, notice.source))
def format_digest(notices: Sequence[Notice]) -> str:
if not notices:
return "No notices."
lines = ["Notification digest"]
lines.extend(
f"- {notice.score:03d} | {notice.source}: {notice.message}"
for notice in notices
)
top = first_or_none(notices)
if top is not None:
lines.append(f"Top source: {top.source}")
return "\n".join(lines)
sources: list[NoticeSource] = [
MemorySource("Python", [("Typing notes", 88), ("Style guide", 62)]),
MemorySource("Testing", [("Regression suite", 91)]),
]
notices = collect(sources, lambda notice: notice.score >= 70)
print(format_digest(notices))
print(f"Empty helper: {first_or_none([]) is None}")
Run python3 notification_digest.py and check:
Notification digest
- 091 | Testing: Regression suite
- 088 | Python: Typing notes
Top source: Testing
Empty helper: True
MemorySource never inherits from NoticeSource; its compatible fetch() is enough for static structural typing. Boundary reminder: hints do not enforce the score range—the dataclass checks it at runtime. Passing a bad callback can also still fail while running.
Three tiny missions
- Narrow a union. Accept
int | str; zero-pad integers and uppercase nonblank strings after anisinstance()check. - Keep the type. Write
last_or_none(items: Sequence[T]) -> T | Noneand try integers andNoticeobjects. - Add a source. Make a tuple-backed class with
fetch()without inheriting fromNoticeSource.
You are ready for Chapter 17 when…
- you can explain that hints normally do not enforce runtime values;
- you can annotate collections by the capabilities they need;
- you can use
Optional/Unionideas and narrow with real checks; - you know a type alias names a shape but does not validate it;
- you can annotate a callback with
Callable; - you can use
TypeVarto preserve an input-output relationship; - you can define and structurally satisfy a small
Protocol; - you know
runtime_checkablechecks presence, not full signatures; - you can run the digest and see
Empty helper: True.
Next, you will see how iterators and generators let a source produce values one at a time.