Warm-up · Activity 1 of 7
// A2.2 · ~30 min · Advanced
Generics with the PEP 695 syntax
After this lesson you can write generic functions and classes with def f[T] and class Box[T], restrict a type parameter with a bound or constraints, and define type aliases with the type statement.
Predict · Activity 2 of 7
Predict before you read on: what does this print?
class Box[T]: def __init__(self, item: T) -> None: self.item = item box = Box[int]("seven") print(box.item, type(box).__name__)Practice · Activity 3 of 7
Make first generic: declare its type parameter T, so the result has the type of the list's items.
def first____(items: list[T]) -> T:def first(items: list[T]) -> T:Practice · Activity 4 of 7
reveal_type() asks mypy to print the type it infers. What does mypy reveal for the last line?
def first[T](items: list[T]) -> T: return items[0] reveal_type(first(["a", "b"]))Practice · Activity 5 of 7
Match each piece of syntax to what it declares.
Brain teaser · Activity 6 of 7
Brain teaser. The alias names Point before the class exists. What does this print?
type Pair = tuple[Point, Point] class Point: pass print(Pair.__value__)Apply · Activity 7 of 7
Mini-task. Write a generic function chunks(items, size) that splits a list into lists of at most size items: chunks([1, 2, 3, 4, 5], 2) gives [[1, 2], [3, 4], [5]]. Annotate it with a type parameter so that mypy knows chunks of a list[str] is a list[list[str]].
Check your work against this list
Build it yourself
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
A generic stack, a bound and a generic alias
Stack[T] holds items of one type; Stack[str]() fixes T to str, so mypy would reject names.push(3). longest accepts anything with a length, because S is bound to Sized. Pair[T] is a generic alias, used in the hints of swap. The last lines show what exists of all this at runtime.
main.py
from collections.abc import Sized
class Stack[T]:
"""A last-in, first-out stack of items of one type."""
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def __len__(self) -> int:
return len(self._items)
def longest[S: Sized](*items: S) -> S:
return max(items, key=len)
type Pair[T] = tuple[T, T]
def swap[T](pair: Pair[T]) -> Pair[T]:
return pair[1], pair[0]
names = Stack[str]()
names.push("Ada")
names.push("Grace")
print(names.pop(), len(names))
print(longest("tea", "coffee", "milk"), longest([1, 2], [3]))
print(swap((1, 2)), Pair[int], Pair.__value__)
print(Stack.__type_params__, longest.__type_params__[0].__bound__)
Run it with
python main.pyOutput
Grace 1
coffee [1, 2]
(2, 1) Pair[int] tuple[T, T]
(T,) <class 'collections.abc.Sized'>- self._items: list[T] uses the class's T inside a method, where it is in scope.
- longest works for strings and lists alike; mypy would reject longest(3, 4), since int has no len().
- Pair[int] prints as Pair[int]: the alias keeps its name. __value__ shows what it stands for.
- __type_params__ and __bound__ are all that the brackets leave at runtime.
Change it and run it
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Exercises
Exercise 1 of 2
Generic helpers
Make both functions generic. first_or(items, default) returns the first item or the default; with a type parameter T it takes list[T] and a T and returns a T. longest(items) returns the first longest item, or None for an empty list; give it a type parameter S bound to Sized, take list[S] and return S | None.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
Declare the parameter after the name: def first_or[T](items: list[T], default: T) -> T:.
Hint 2
A bound goes after a colon inside the brackets: def longest[S: Sized](...).
Hint 3
The return hint of longest is S | None: the item type itself, or None.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
from collections.abc import Sized
def first_or[T](items: list[T], default: T) -> T:
return items[0] if items else default
def longest[S: Sized](items: list[S]) -> S | None:
if not items:
return None
return max(items, key=len)
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
from collections.abc import Sized
def first_or(items: list[object], default: object) -> object:
return items[0] if items else default
def longest(items: list[Sized]) -> Sized | None:
if not items:
return None
return max(items, key=len)
test_main.py
from collections.abc import Sized
from typing import get_type_hints
from main import first_or, longest
def test_first_or_is_generic():
"""first_or has one type parameter, T, used in all its hints"""
params = first_or.__type_params__
assert len(params) == 1, f"first_or.__type_params__ is {params!r}, expected one parameter"
t = params[0]
got = get_type_hints(first_or)
assert got == {"items": list[t], "default": t, "return": t}, f"the hints of first_or are {got!r}"
def test_first_or_behaviour():
"""first_or gives the first item, or the default for an empty list"""
got = first_or([3, 4], 0), first_or([], "none")
assert got == (3, "none"), f"first_or returned {got!r}, expected (3, 'none')"
def test_longest_bound():
"""longest has one type parameter, bound to Sized"""
params = longest.__type_params__
assert len(params) == 1 and params[0].__bound__ is Sized, f"longest.__type_params__ is {params!r}; expected one parameter bound to Sized"
s = params[0]
got = get_type_hints(longest)
assert got == {"items": list[s], "return": s | None}, f"the hints of longest are {got!r}"
def test_longest_behaviour():
"""longest returns the first longest item, or None for an empty list"""
got = longest(["tea", "coffee", "cocoa!"]), longest([{1}, {2, 3}]), longest([])
assert got == ("coffee", {2, 3}, None), f"longest returned {got!r}"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 2 of 2
A generic queue with a batch alias
Turn Queue into a generic class Queue[T]: put takes a T, get returns a T. Add a generic alias Batch[T] for list[T] with the type statement, and implement take(n): it removes up to n items from the front and returns them, annotated to return Batch[T]. take never fails when fewer items are left.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
The alias takes its own parameter: type Batch[T] = list[T]. The class declares its own T as well: class Queue[T]:.
Hint 2
Replace object by T in the list and in put and get, and annotate take with -> Batch[T].
Hint 3
Slicing never fails when the list is short: taken = self._items[:n], then del self._items[:n].
Show a solution
One way to solve it. Yours can look different and still pass the checks.
type Batch[T] = list[T]
class Queue[T]:
"""First in, first out."""
def __init__(self) -> None:
self._items: list[T] = []
def put(self, item: T) -> None:
self._items.append(item)
def get(self) -> T:
return self._items.pop(0)
def take(self, n: int) -> Batch[T]:
taken = self._items[:n]
del self._items[:n]
return taken
def __len__(self) -> int:
return len(self._items)
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
# Add the alias here: Batch[T] stands for list[T].
class Queue:
"""First in, first out."""
def __init__(self) -> None:
self._items: list[object] = []
def put(self, item: object) -> None:
self._items.append(item)
def get(self) -> object:
return self._items.pop(0)
def take(self, n: int) -> list[object]:
# Remove up to n items from the front and return them.
return []
def __len__(self) -> int:
return len(self._items)
test_main.py
from typing import TypeAliasType
import main
from main import Queue
def test_batch_alias():
"""Batch is a generic type alias for list[T]"""
batch = getattr(main, "Batch", None)
assert isinstance(batch, TypeAliasType), f"main.Batch is {batch!r}, expected an alias from a type statement"
assert len(batch.__type_params__) == 1, f"Batch has the type parameters {batch.__type_params__!r}, expected one"
t = batch.__type_params__[0]
assert batch.__value__ == list[t], f"Batch stands for {batch.__value__!r}, expected list[T]"
def test_queue_is_generic():
"""Queue has one type parameter, and take() returns Batch[T]"""
params = Queue.__type_params__
assert len(params) == 1, f"Queue.__type_params__ is {params!r}, expected one parameter"
got = Queue.take.__annotations__["return"]
assert got == main.Batch[params[0]], f"take() is annotated to return {got!r}, expected Batch[T]"
def test_first_in_first_out():
"""get() returns the items in the order they were put"""
queue = Queue[str]()
for word in ["a", "b", "c"]:
queue.put(word)
got = queue.get(), queue.get(), len(queue)
assert got == ("a", "b", 1), f"two get() calls and len() gave {got!r}, expected ('a', 'b', 1)"
def test_take():
"""take(2) removes the two oldest items, and take(5) the one left"""
queue = Queue[int]()
for n in [1, 2, 3]:
queue.put(n)
got = queue.take(2), queue.take(5), len(queue)
assert got == ([1, 2], [3], 0), f"take(2), take(5) and len() gave {got!r}, expected ([1, 2], [3], 0)"
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyCommon mistakes
Using isinstance with a type parameter
def keep[T](items: list[object], kind: T) -> list[object]:
return [item for item in items if isinstance(item, T)]
print(keep([1, "a", 2], int))
What Python prints
TypeError: isinstance() arg 2 must be a type, a tuple of types, or a unionWhy, and the fix
At runtime T is a typing.TypeVar, a placeholder for the checker, not the class passed in. Pass the class itself and annotate it as such: def keep[T](items: list[object], kind: type[T]) -> list[T]:, then test isinstance(item, kind).
Calling a type alias like a class
type Grid = list[list[int]]
grid = Grid()
What Python prints
TypeError: 'typing.TypeAliasType' object is not callableWhy, and the fix
A type statement creates a TypeAliasType for annotations, not a class you can instantiate. Build the value with the real type and use the alias in the hint: grid: Grid = []. If you need a callable name, a plain assignment such as Grid = list is a different thing, with no lazy evaluation.
Mixing the new syntax with Generic[T]
from typing import Generic, TypeVar
T = TypeVar("T")
class Box[T](Generic[T]):
pass
What Python prints
TypeError: Cannot inherit from Generic[...] multiple times.Why, and the fix
class Box[T]: already adds Generic as a base class, so listing Generic[T] again adds it twice. Use one style: either class Box[T]: with no TypeVar at all, or the older T = TypeVar("T") with class Box(Generic[T]):. New code in Python 3.12 and later uses the brackets.
Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source
Exit ticket
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.