🚀
Advanced
Annotations · TypeVar · Generic · dataclass · Protocol
Week 1 — Type Hints & Dataclasses
Add static type information to Python code with type hints, use TypeVar for generic functions, model data with @dataclass, and define structural subtypes with Protocol.
typingtype hintsdataclassProtocolGeneric
Duration
⏱ 2.5 hours
Level
📊 Advanced
Prerequisite
🎯 Intermediate complete
OUTCOME
Write a type-annotated data pipeline that passes mypy --strict
What you'll learn
- 1Annotate function signatures and variables
- 2Use Optional, Union, List, Dict, Tuple from typing
- 3Write generic functions with TypeVar
- 4Model immutable records with frozen dataclasses
- 5Define structural contracts with Protocol
1. Type Hints
python
from typing import Optional, Union
def greet(name: str, times: int = 1) -> str:
return (name + "\n") * times
def find_user(user_id: int) -> Optional[dict]:
db = {1: {"name": "Alice"}}
return db.get(user_id)
# Union (Python 3.10+: use | directly)
def stringify(value: Union[int, float, str]) -> str:
return str(value)ℹ️
Type hints don't affect runtime behaviour — Python still runs without them. Use mypy or pyright for static checking.
2. Generic Functions
python
from typing import TypeVar, List
T = TypeVar("T")
def first(lst: List[T]) -> Optional[T]:
return lst[0] if lst else None
print(first([1, 2, 3])) # 1 — inferred as int
print(first(["a", "b"])) # "a" — inferred as str3. Dataclasses
python
from dataclasses import dataclass, field
from typing import List
@dataclass(frozen=True) # immutable
class Point:
x: float
y: float
@dataclass
class Team:
name: str
members: List[str] = field(default_factory=list)
def add(self, member: str) -> None:
self.members.append(member)
t = Team("Alpha")
t.add("Alice")
t.add("Bob")
print(t) # Team(name='Alpha', members=['Alice', 'Bob'])4. Protocol
python
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> str: ...
class Circle:
def draw(self) -> str: return "O"
class Square:
def draw(self) -> str: return "[]"
def render(shape: Drawable) -> None:
print(shape.draw())
render(Circle()) # O
render(Square()) # []💻 Examples
Run these examples and check the output yourself.
01_typed_pipeline.py— Type-annotated data processing pipeline
CODE
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Record:
name: str
score: float
active: bool = True
def load(raw: List[dict]) -> List[Record]:
return [Record(**r) for r in raw]
def top_n(records: List[Record], n: int = 3) -> List[Record]:
active = [r for r in records if r.active]
return sorted(active, key=lambda r: r.score, reverse=True)[:n]
raw = [
{"name": "Alice", "score": 92.5},
{"name": "Bob", "score": 87.0},
{"name": "Carol", "score": 95.1, "active": False},
{"name": "Dave", "score": 88.3},
]
records = load(raw)
for r in top_n(records):
print(f"{r.name}: {r.score}")
▶ Output
Alice: 92.5
Dave: 88.3
Bob: 87.0📝 Exercises
Try them yourself first, then open the solution to compare.
Exercise 1
Type-safe Stack
Goal: Implement a generic Stack[T] class that passes mypy.
Requirements
- Generic[T] base class
- push(item: T), pop() -> T, peek() -> T, is_empty() -> bool
- Raises IndexError on pop/peek from empty stack
- Fully annotated
▶Toggle solution
SOLUTION
from typing import Generic, TypeVar, List
T = TypeVar("T")
class Stack(Generic[T]):
def __init__(self) -> None:
self._data: List[T] = []
def push(self, item: T) -> None:
self._data.append(item)
def pop(self) -> T:
if self.is_empty(): raise IndexError("Stack is empty")
return self._data.pop()
def peek(self) -> T:
if self.is_empty(): raise IndexError("Stack is empty")
return self._data[-1]
def is_empty(self) -> bool:
return len(self._data) == 0
s: Stack[int] = Stack()
s.push(1); s.push(2); s.push(3)
print(s.pop(), s.pop())
▶ Output
3 2Example code / lecture materials
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗