Skip to content
← All Python articles

Python explainer

Choose Between Iterable, Iterator and Sequence

About 5 min to read. Allow extra time to work through examples.

Pick the type hint from what your function does with its input, so mypy catches a generator passed to code that loops over it twice.

This article uses Python. You can make it your practice mode to browse related content across the site.

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

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.

step 6 of 9

Generators, Iterators and Streaming Data

Up next Copy a One-Shot Iterator into Separate Lists