This function turns counts into shares of the total:
from collections.abc import Iterable
def shares(counts: Iterable[int]) -> list[float]:
total = sum(counts)
return [c / total for c in counts]
print(shares([2, 3, 5])) # [0.2, 0.3, 0.5]
print(shares(c for c in [2, 3, 5])) # []
With a list it works. With a generator it returns an empty list and raises no
error. The sum call used up the generator, so the second loop had nothing to
read. You met this bug in
Generators and One-Shot Iterators.
This time mypy accepts both calls, because the hint says the function needs
only one pass over counts. The body makes two.
A type hint on a parameter tells callers, and mypy, what the function does with the argument. Pick it from the body, and mypy catches the bad call.
Three hints for things you can loop over
All three come from collections.abc:
| Hint | What the function may do | Examples |
|---|---|---|
Iterable[T] |
Loop over it once | list, set, dict, a generator, an open file |
Iterator[T] |
Call next() on it, which moves it forward for good |
a generator, an open file, iter(some_list) |
Sequence[T] |
Call len(), read x[i] and x[a:b], and loop over it many times |
list, tuple, str, range |
Every iterator is also an iterable, so a parameter typed Iterable[T] accepts
a generator. A list is a sequence and an iterable, but not an iterator.
Collection[T] asks for more than Iterable and less than Sequence. It
allows len() and in without indexing, so it also accepts a set or a dict.
Pick the hint from what the body does
The shares body loops twice, so it needs a sequence. Change the hint, and
mypy reports the bad call before the code runs:
from collections.abc import Sequence
def shares(counts: Sequence[int]) -> list[float]:
total = sum(counts)
return [c / total for c in counts]
shares([2, 3, 5])
shares(c for c in [2, 3, 5])
# error: Argument 1 to "shares" has incompatible type
# "Generator[int, None, None]"; expected "Sequence[int]"
You can also keep Iterable[int] and copy the input once at the top, with
values = list(counts). That stores the whole input, which is fine for a page
of numbers and wrong for a 10 GB log. No hint can say that an input is small
enough to copy, so say it in the docstring.
When one pass is enough, keep Iterable and do all the work in that pass:
from collections.abc import Iterable
def mean(values: Iterable[float]) -> float:
total = 0.0
count = 0
for value in values:
total += value
count += 1
if count == 0:
raise ValueError("mean of no values")
return total / count
print(mean(v for v in [2.0, 4.0])) # 3.0
For a return value, describe what you hand back. A generator function returns
Iterator[T], which tells the caller they get one pass. A function that builds
a list returns list[T], so the caller knows they can index it and loop again.
Common mistakes
Asking for Iterator when any iterable will do. If a parameter is
Iterator[str], mypy rejects a call with ["a", "b"], because a list has no
__next__ method. Ask for Iterator[T] only when the caller hands you their
iterator and expects you to move it forward. For example, your function reads the
header line of an open file with next(), and the caller reads the rest. If
you only want next() for yourself, ask for Iterable[T] and call iter()
on it first.
Passing one string where you meant a list of strings. A string is a sequence
of one-character strings, so it matches Iterable[str] and Sequence[str]:
from collections.abc import Iterable
def render(names: Iterable[str]) -> str:
if isinstance(names, str):
raise TypeError("pass a list of names, not one string")
return ", ".join(names)
print(render(["alice", "bob"])) # alice, bob
Without the check, render("alice") returns "a, l, i, c, e", and mypy has no
complaint. A list[str] hint would reject a string, but it would also reject a
tuple and a generator. Bytes have a similar trap, because looping over b"hi"
gives the integers 104 and 105.
Check your understanding
A function returns sorted(scores, reverse=True)[:3]. Should scores be Iterable[int] or Sequence[int]?
Show answer
Hide answer
Iterable[int]. The sorted() function reads its input once and builds a new
list of its own, so a generator works. Asking for Sequence[int] would make
callers build a list the function does not need. The body uses one pass, so the
hint asks for one pass.
Going further
You can also annotate a generator function as Generator[Y, S, R]. The three
parts are the type it yields, the type its send() method accepts, and the
type it returns when it finishes. Use Iterator[Y] unless callers need
send(), throw() or close(). Python 3.13 gave the last two parts a default
of None, so from 3.13 on, Generator[int] means Generator[int, None, None].
If your code must run on Python 3.12, write all three.
At run time, isinstance(x, Iterable) checks for an __iter__ method. A few
older classes support loops through __getitem__ alone, and that check misses
them. Calling iter(x) and catching TypeError is the reliable test.
The collections.abc documentation lists each of these types and the methods it needs.