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

names
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))

  1. 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
  1. 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
  1. 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}")

  1. 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'): None
  2. for name, age in ages.items():

    pass 1 of 3
    16# Iteration17for nameAlice, age31 in ages{'Alice': 31, 'Bob': 27, 'Charlie': 40}.items():18    print(f"  {nameAlice} -> {age31}")
    output  Alice -> 31
    All 3 passes — pass 1 is the card above
    passnameage
    1Alice31
    2Bob27
    3Charlie40

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)

  1. 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)

  1. 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 for list, tuple, etc.)
  • Mapping[K, V] - read-only-ish mapping interface (works for dict)
  • 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}))

  1. 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)))
  2. def total(values: Iterable[int]) -> int:

    pass 1 of 2
    7def total(values[1, 2, 3]: Iterable[int]) -> int:8    return sum(values[1, 2, 3])
  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]) = 6
  4. def total(values: Iterable[int]) -> int:

    pass 2 of 2
    7def total(values(1, 2, 3): Iterable[int]) -> int:8    return sum(values(1, 2, 3))
  5. 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)) = 6
  6. def first_item(values: Sequence[str]) -> str:

    pass 1 of 2
    11def first_item(values['a', 'b']: Sequence[str]) -> str:12    return values[0]a
  7. print("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']) = a
  8. def first_item(values: Sequence[str]) -> str:

    pass 2 of 2
    11def first_item(values('x', 'y'): Sequence[str]) -> str:12    return values[0]x
  9. print("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')) = x
  10. parts ← ['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'])
  11. 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