Type Hints
Type Hints for Collections
When you pass a list of user IDs to a function, how does the function know they should be integers? Collection type hints like list[int] and dict[str, User] make data structures self-documenting and catch type mismatches before they cause runtime errors.
Python lets you type common containers like list, dict, set, and tuple.
List Types
list.py
Replay: real traced execution (multi-file project)
# list[T]
# Typed list
names: list[str] = ["Alice", "Bob", "Charlie"]
print("names:", names)
# Append/extend
names.append("Dora")
names.extend(["Eve", "Frank"])
print("after append/extend:", names)
# Indexing and slicing
first: str = names[0]
last_two: list[str] = names[-2:]
print("first:", first)
print("last_two:", last_two)
# List of numbers
scores: list[int] = [10, 20, 15]
print("scores:", scores)
print("sum:", sum(scores))
# list[T]
# Typed list
names: list[str] = ["Maya", "Noah"]
print("names:", names)
# Append/extend
names.append("Dora")
names.extend(["Eve", "Frank"])
print("after append/extend:", names)
# Indexing and slicing
first: str = names[0]
last_two: list[str] = names[-2:]
print("first:", first)
print("last_two:", last_two)
# List of numbers
scores: list[int] = [10, 20, 15]
print("scores:", scores)
print("sum:", sum(scores))
# list[T]
# Typed list
names: list[str] = ["Ada", "Linus", "Grace"]
print("names:", names)
# Append/extend
names.append("Dora")
names.extend(["Eve", "Frank"])
print("after append/extend:", names)
# Indexing and slicing
first: str = names[0]
last_two: list[str] = names[-2:]
print("first:", first)
print("last_two:", last_two)
# List of numbers
scores: list[int] = [10, 20, 15]
print("scores:", scores)
print("sum:", sum(scores))
names ← ['Alice', 'Bob', 'Charlie'], first ← Alice, last_two ← ['Eve', 'Frank']
3# Typed list4names→ ['Alice', 'Bob', 'Charlie']: list[str] = ["Alice", "Bob", "Charlie"]5#@names=["Maya", "Noah"], ["Ada", "Linus", "Grace"]6print("names:", names['Alice', 'Bob', 'Charlie'])78# Append/extend9names→ ['Alice', 'Bob', 'Charlie', 'Dora'].append("Dora")10names→ ['Alice', 'Bob', 'Charlie', 'Dora', 'Eve', 'Frank'].extend(["Eve", "Frank"])11print("after append/extend:", names['Alice', 'Bob', 'Charlie', 'Dora', 'Eve', 'Frank'])1213# Indexing and slicing14first→ Alice: str = names[0]Alice15last_two→ ['Eve', 'Frank']: list[str] = names[-2:]['Eve', 'Frank']16print("first:", firstAlice)17print("last_two:", last_two['Eve', 'Frank'])1819# List of numbers20scores→ [10, 20, 15]: list[int] = [10, 20, 15]21print("scores:", scores[10, 20, 15])22print("sum:", sum(scores[10, 20, 15]))outputnames: ['Alice', 'Bob', 'Charlie'] after append/extend: ['Alice', 'Bob', 'Charlie', 'Dora', 'Eve', 'Frank'] first: Alice last_two: ['Eve', 'Frank'] scores: [10, 20, 15] sum: 45
names ← ['Maya', 'Noah'], first ← Maya, last_two ← ['Eve', 'Frank']
3# Typed list4names→ ['Maya', 'Noah']: list[str] = ["Maya", "Noah"]5print("names:", names['Maya', 'Noah'])67# Append/extend8names→ ['Maya', 'Noah', 'Dora'].append("Dora")9names→ ['Maya', 'Noah', 'Dora', 'Eve', 'Frank'].extend(["Eve", "Frank"])10print("after append/extend:", names['Maya', 'Noah', 'Dora', 'Eve', 'Frank'])1112# Indexing and slicing13first→ Maya: str = names[0]Maya14last_two→ ['Eve', 'Frank']: list[str] = names[-2:]['Eve', 'Frank']15print("first:", firstMaya)16print("last_two:", last_two['Eve', 'Frank'])1718# List of numbers19scores→ [10, 20, 15]: list[int] = [10, 20, 15]20print("scores:", scores[10, 20, 15])21print("sum:", sum(scores[10, 20, 15]))outputnames: ['Maya', 'Noah'] after append/extend: ['Maya', 'Noah', 'Dora', 'Eve', 'Frank'] first: Maya last_two: ['Eve', 'Frank'] scores: [10, 20, 15] sum: 45
names ← ['Ada', 'Linus', 'Grace'], first ← Ada, last_two ← ['Eve', 'Frank']
3# Typed list4names→ ['Ada', 'Linus', 'Grace']: list[str] = ["Ada", "Linus", "Grace"]5print("names:", names['Ada', 'Linus', 'Grace'])67# Append/extend8names→ ['Ada', 'Linus', 'Grace', 'Dora'].append("Dora")9names→ ['Ada', 'Linus', 'Grace', 'Dora', 'Eve', 'Frank'].extend(["Eve", "Frank"])10print("after append/extend:", names['Ada', 'Linus', 'Grace', 'Dora', 'Eve', 'Frank'])1112# Indexing and slicing13first→ Ada: str = names[0]Ada14last_two→ ['Eve', 'Frank']: list[str] = names[-2:]['Eve', 'Frank']15print("first:", firstAda)16print("last_two:", last_two['Eve', 'Frank'])1718# List of numbers19scores→ [10, 20, 15]: list[int] = [10, 20, 15]20print("scores:", scores[10, 20, 15])21print("sum:", sum(scores[10, 20, 15]))outputnames: ['Ada', 'Linus', 'Grace'] after append/extend: ['Ada', 'Linus', 'Grace', 'Dora', 'Eve', 'Frank'] first: Ada last_two: ['Eve', 'Frank'] scores: [10, 20, 15] sum: 45
generic type - a parameterized type like `list[T]` where `T` specifies the element type
Dictionary Types
dict.py
Replay: real traced execution (multi-file project)
# dict[K, V]
# Typed dict
ages: dict[str, int] = {"Alice": 30, "Bob": 27}
print("ages:", ages)
# Insert/update
ages["Charlie"] = 40
ages["Alice"] = ages["Alice"] + 1
print("after update:", ages)
# Lookup with get
maybe_age: int | None = ages.get("Dora")
print("ages.get('Dora'):", maybe_age)
# Iteration
for name, age in ages.items():
print(f" {name} -> {age}")
ages ← {'Alice': 30, 'Bob': 27}, ages[”Charlie”] ← 40, ages[”Alice”] ← 31
3# Typed dict4ages→ {'Alice': 30, 'Bob': 27}: dict[str, int] = {"Alice": 30, "Bob": 27}5print("ages:", ages{'Alice': 30, 'Bob': 27})67# Insert/update8ages["Charlie"]→ 40 = 409ages["Alice"]→ 31 = ages["Alice"] + 110print("after update:", ages{'Alice': 31, 'Bob': 27, 'Charlie': 40})1112# Lookup with get13maybe_age→ None: int | None = ages{'Alice': 31, 'Bob': 27, 'Charlie': 40}.get("Dora")14print("ages.get('Dora'):", maybe_ageNone)outputages: {'Alice': 30, 'Bob': 27} after update: {'Alice': 31, 'Bob': 27, 'Charlie': 40} ages.get('Dora'): Nonefor name, age in ages.items():
pass 1 of 316# Iteration17for nameAlice, age31 in ages{'Alice': 31, 'Bob': 27, 'Charlie': 40}.items():18 print(f" {nameAlice} -> {age31}")output Alice -> 31All 3 passes — pass 1 is the card above pass nameage1 Alice 31 2 Bob 27 3 Charlie 40
Set and Tuple Types
set_tuple.py
Replay: real traced execution (multi-file project)
# set[T] and tuple[...]
# set[str]
tags: set[str] = {"python", "typing", "python"}
print("tags:", tags)
# set operations
more: set[str] = {"docs", "typing"}
print("union:", tags | more)
print("intersection:", tags & more)
# tuple with fixed length/types
point: tuple[float, float] = (1.5, 2.0)
print("point:", point)
# tuple with variable length
numbers: tuple[int, ...] = (1, 2, 3, 4)
print("numbers:", numbers)
tags ← {'python', 'typing'}, more ← {'typing', 'docs'}, point ← (1.5, 2.0)
3# set[str]4tags→ {'python', 'typing'}: set[str] = {"python", "typing", "python"}5print("tags:", tags{'python', 'typing'})67# set operations8more→ {'typing', 'docs'}: set[str] = {"docs", "typing"}9print("union:", tags{'python', 'typing'} | more{'typing', 'docs'})10print("intersection:", tags{'python', 'typing'} & more{'typing', 'docs'})1112# tuple with fixed length/types13point→ (1.5, 2.0): tuple[float, float] = (1.5, 2.0)14print("point:", point(1.5, 2.0))1516# tuple with variable length17numbers→ (1, 2, 3, 4): tuple[int, ...] = (1, 2, 3, 4)18print("numbers:", numbers(1, 2, 3, 4))outputtags: {'python', 'typing'} union: {'python', 'typing', 'docs'} intersection: {'typing'} point: (1.5, 2.0) numbers: (1, 2, 3, 4)
tuple type - fixed-length `tuple[T1, T2]` for heterogeneous data, or `tuple[T, ...]` for variable-length
Nested Collections
nested.py
Replay: real traced execution (multi-file project)
# Nested collection types
# list[dict[str, int]]
rows: list[dict[str, int]] = [
{"id": 1, "score": 10},
{"id": 2, "score": 30},
{"id": 3, "score": 20},
]
print("rows:", rows)
# Compute aggregate
scores: list[int] = [r["score"] for r in rows]
print("scores:", scores)
print("avg score:", sum(scores) / len(scores))
# dict[str, list[str]]
groups: dict[str, list[str]] = {
"admins": ["Alice", "Bob"],
"users": ["Charlie"],
}
print("groups:", groups)
# Safe updates
new_user: str = "Dora"
groups.setdefault("users", []).append(new_user)
print("after add:", groups)
rows ← [{'id': 1, 'score': 10}, {'id': 2, 'score': 30}, {'id': 3, 'score': 20}]
3# list[dict[str, int]]4rows→ [{'id': 1, 'score': 10}, {'id': 2, 'score': 30}, {'id': 3, 'score': 20}]: list[dict[str, int]] = [5 {"id": 1, "score": 10},6 {"id": 2, "score": 30},7 {"id": 3, "score": 20},8]9print("rows:", rows[{'id': 1, 'score': 10}, {'id': 2, 'score': 30}, {'id': 3, 'score': 20}])1011# Compute aggregate12scores→ [10, 30, 20]: list[int] = [r["score"](empty) for r in rows[{'id': 1, 'score': 10}, {'id': 2, 'score': 30}, {'id': 3, 'score': 20}]]13print("scores:", scores[10, 30, 20])14print("avg score:", sum(scores[10, 30, 20]) / len(scores))1516# dict[str, list[str]]17groups→ {'admins': ['Alice', 'Bob'], 'users': ['Charlie']}: dict[str, list[str]] = {18 "admins": ["Alice", "Bob"],19 "users": ["Charlie"],20}21print("groups:", groups{'admins': ['Alice', 'Bob'], 'users': ['Charlie']})2223# Safe updates24new_user→ Dora: str = "Dora"25groups→ {'admins': ['Alice', 'Bob'], 'users': ['Charlie', 'Dora']}.setdefault("users", []).append(new_userDora)26print("after add:", groups{'admins': ['Alice', 'Bob'], 'users': ['Charlie', 'Dora']})outputrows: [{'id': 1, 'score': 10}, {'id': 2, 'score': 30}, {'id': 3, 'score': 20}] scores: [10, 30, 20] avg score: 20.0 groups: {'admins': ['Alice', 'Bob'], 'users': ['Charlie']} after add: {'admins': ['Alice', 'Bob'], 'users': ['Charlie', 'Dora']}
Abstract Collection Types
From typing:
Sequence[T]- read-only-ish sequence interface (works forlist,tuple, etc.)Mapping[K, V]- read-only-ish mapping interface (works fordict)Iterable[T]- any iterable input
abstract_types.py
Replay: real traced execution (multi-file project)
# Abstract collection types
from typing import Iterable, Mapping, Sequence
# Prefer abstract types in parameters
def total(values: Iterable[int]) -> int:
return sum(values)
def first_item(values: Sequence[str]) -> str:
return values[0]
def describe(mapping: Mapping[str, int]) -> str:
parts = [f"{k}={v}" for k, v in mapping.items()]
return ", ".join(parts)
print("total([1,2,3]) =", total([1, 2, 3]))
print("total((1,2,3)) =", total((1, 2, 3)))
print("first_item(['a','b']) =", first_item(["a", "b"]))
print("first_item(('x','y')) =", first_item(("x", "y")))
print("describe({'a':1,'b':2}) =", describe({"a": 1, "b": 2}))
print("total([1,2,3]) =", total([1, 2, 3]))
19print("total([1,2,3]) =", total([1, 2, 3]))20print("total((1,2,3)) =", total((1, 2, 3)))def total(values: Iterable[int]) -> int:
pass 1 of 27def total(values[1, 2, 3]: Iterable[int]) -> int:8 return sum(values[1, 2, 3])print("total([1,2,3]) =", total([1, 2, 3]))
19print("total([1,2,3]) =", total([1, 2, 3]))20print("total((1,2,3)) =", total((1, 2, 3)))outputtotal([1,2,3]) = 6def total(values: Iterable[int]) -> int:
pass 2 of 27def total(values(1, 2, 3): Iterable[int]) -> int:8 return sum(values(1, 2, 3))print("total((1,2,3)) =", total((1, 2, 3)))
19print("total([1,2,3]) =", total([1, 2, 3]))20print("total((1,2,3)) =", total((1, 2, 3)))2122print("first_item(['a','b']) =", first_item(["a", "b"]))23print("first_item(('x','y')) =", first_item(("x", "y")))outputtotal((1,2,3)) = 6def first_item(values: Sequence[str]) -> str:
pass 1 of 211def first_item(values['a', 'b']: Sequence[str]) -> str:12 return values[0]aprint("first_item(['a','b']) =", first_item(["a", "b"]))
22print("first_item(['a','b']) =", first_item(["a", "b"]))23print("first_item(('x','y')) =", first_item(("x", "y")))outputfirst_item(['a','b']) = adef first_item(values: Sequence[str]) -> str:
pass 2 of 211def first_item(values('x', 'y'): Sequence[str]) -> str:12 return values[0]xprint("first_item(('x','y')) =", first_item(("x", "y")))
22print("first_item(['a','b']) =", first_item(["a", "b"]))23print("first_item(('x','y')) =", first_item(("x", "y")))2425print("describe({'a':1,'b':2}) =", describe({"a": 1, "b": 2}))outputfirst_item(('x','y')) = xparts ← ['a=1', 'b=2']
15def describe(mapping{'a': 1, 'b': 2}: Mapping[str, int]) -> str:16 parts→ ['a=1', 'b=2'] = [f"{k(empty)}={v(empty)}" for k, v in mapping{'a': 1, 'b': 2}.items()]17 return ", ".join(parts['a=1', 'b=2'])print("describe({'a':1,'b':2}) =", describe({"a": 1, "b": 2}))
25print("describe({'a':1,'b':2}) =", describe({"a": 1, "b": 2}))outputdescribe({'a':1,'b':2}) = a=1, b=2
Prefer abstract types for function parameters when you only need a subset of behavior.
abstract types - types like `Sequence[T]` and `Mapping[K, V]` that accept multiple concrete types
Exercise: practical.py
Type a function that processes nested configuration data