We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← The Type System as a Design Tool step 10 of 24
Generic partition and chunk
A module-scope T = TypeVar("T") imported across five modules is a shared
mutable definition dressed as a type. Change its variance or its bound in one
place and the meaning of every signature that mentions it changes, silently,
everywhere. There is no import graph you can look at to find out who was
affected, because T looks like a boring constant.
PEP 695 (Python 3.12) makes the type parameter lexically scoped to the thing that declares it:
def partition[T](items: Iterable[T], predicate: Callable[[T], bool]) -> tuple[list[T], list[T]]:
The [T] is a real scope. Two functions each declaring [T] share nothing. At
runtime the function gains a __type_params__ attribute, and — worth knowing
because it surprises people — 'T' in locals() inside the body is False. The
parameter is not a local variable; it lives in an implicit enclosing scope
created just for the annotations.
Variance is inferred rather than declared, which is a separate topic, but the practical upshot for functions is: you almost never think about it.
The task
def partition[T](
items: Iterable[T],
predicate: Callable[[T], bool],
) -> tuple[list[T], list[T]]: ...
def chunk[T](items: Sequence[T], size: int) -> list[list[T]]: ...
def solve(
items: list[int],
threshold: int,
size: int,
) -> tuple[list[int], list[int], list[list[int]], str]: ...
partition returns (matching, rest), each preserving input order. It
takes Iterable[T] because it iterates exactly once — a generator, a set, a
dict.items() view all work.
chunk splits a Sequence[T] into consecutive lists of at most size
elements, the last one possibly short. It takes Sequence[T] rather than
Iterable[T] because it slices and needs len. size < 1 raises ValueError
with the message "size must be >= 1".
solve returns four things:
-
the elements
>= threshold, viapartition; - the rest;
-
chunk(items, size); -
the message produced by calling
chunk(items, 0)— the guard’s own error string, or""if it did not fire.
The fourth element is there so the guard is actually tested rather than merely documented. Error messages are part of your API; a caller greps for them.
Pass iter(items) to partition in solve. It costs nothing and it proves the
parameter really is an Iterable, not a list in disguise.
Why the parameter types differ
This is the choice you will be asked to defend in review, and the rule is short:
-
iterate once, no indexing, no
len->Iterable[T] -
index or need
len->Sequence[T] -
read a mapping ->
Mapping[K, V] -
mutate ->
list[T], and its invariance is doing its job
Return list[T] and dict[K, V]. Accept broad, return narrow: the caller can
always widen what you hand back, but cannot narrow what you demanded.
Types
The element type must survive the call. partition([1, 2, 3], f)[0] is
list[int], not list[Any] — if it comes back as Any, the annotation is
decorative and every downstream call site is unchecked. Similarly
chunk(["a"], 1) is list[list[str]].
--disallow-any-generics will reject a bare list or Callable anywhere in
your signatures, and Generic must not appear — the whole point of PEP 695 is
that it is no longer needed.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.