Back to Blog
Python

Python Sequence Type Hints: Sequence, List, Tuple

python sequence type hint: Learn how to type hint Python sequences with typing.Sequence, list, and tuple, and when to use each for better static analysis.

type hintstypingsequencesstatic analysismypy
Illustration of Python sequence type hints showing list, tuple, and Sequence abstract type with arrows indicating compatibility.

When you annotate a function parameter that accepts a list, a tuple, or any ordered collection, the choice between list, tuple, and typing.Sequence changes what callers can pass and what your code can safely do with the value. A python sequence type hint is not just a syntax detail; it communicates intent to static checkers like mypy, pyright, and to other developers reading the signature.

Why Sequence Type Hints Matter

Python's type hints are optional, but once you use them, they become a contract. If you annotate a parameter as list[int], you are saying the caller must pass a list instance. That excludes tuples, ranges, and other sequence types even though they support the same iteration and indexing operations. Many functions only need to read a sequence, not mutate it. Using the wrong hint forces callers to convert their data unnecessarily or forces you to accept a narrower set of inputs than your logic actually supports.

The typing module provides Sequence, an abstract base class that represents any ordered, indexable collection. It is covariant in its element type, meaning Sequence[int] is a supertype of both list[int] and tuple[int, ...]. This makes it the natural choice for read-only parameters.

Using typing.Sequence for Read-Only Sequences

When your function only iterates over the input or accesses it by index, Sequence is the most flexible hint. It accepts lists, tuples, range, memoryview, and any other class that implements the sequence protocol. Here is a typical example:

from typing import Sequence def total(values: Sequence[float]) -> float: return sum(values)

This function works with [1.5, 2.5], (1.0, 2.0), or range(10). If you annotated it with list[float], the tuple and range would be rejected by a static checker, even though the function never mutates the input. Using Sequence avoids that unnecessary restriction.

Note that Sequence does not guarantee mutability. You cannot call append or pop on a Sequence value because those methods are not part of the Sequence protocol. If your function needs to modify the collection, you must use list or MutableSequence (though the latter is rarely used).

When to Use List and Tuple Directly

Use list[T] when the parameter is meant to be mutated in place, or when the function explicitly requires a list because it relies on list-specific methods like append or sort. For example:

def add_item(items: list[str], item: str) -> None: items.append(item)

Here Sequence would be wrong because it does not guarantee an append method. Similarly, use tuple[T, ...] when the length is fixed and the positions have distinct meanings. A tuple hint like tuple[int, str] is a different type from tuple[int, ...]; the former is a fixed-length pair, the latter is a variable-length homogeneous tuple.

If you are returning a sequence from a function, prefer to return a concrete type like list[T] or tuple[T, ...] rather than Sequence[T]. Returning Sequence would force callers to accept an abstract type, which is fine for reading but prevents them from using list-specific operations without a cast.

Handling Nested Sequences

Nested sequences, such as a list of tuples or a list of lists, require careful nesting of the type hints. For example, a function that processes rows of a table might take Sequence[Sequence[int]]:

from typing import Sequence def row_sums(rows: Sequence[Sequence[int]]) -> list[int]: return [sum(row) for row in rows]

This accepts a list of lists, a tuple of tuples, or a list of tuples, as long as each inner element is itself a sequence of integers. The inner Sequence allows both list and tuple rows. If you need the inner sequences to be mutable, you would use Sequence[list[int]] or list[list[int]] depending on the outer requirement.

One common mistake is using Sequence for the outer layer but then indexing into the inner element and expecting it to be a list. Since the inner type is also Sequence, you cannot call list-only methods on it. If you need that, change the inner hint to list.

Type Variance and Sequence Subtyping

Sequence is covariant in its element type. This means Sequence[int] is a subtype of Sequence[object], and a function that accepts Sequence[object] can be called with a list of integers. list is invariant, so list[int] is not a subtype of list[object] in a static type system. This has practical consequences when you design APIs that accept sequences.

Consider a function that logs any sequence:

def log_values(values: Sequence[object]) -> None: for v in values: print(v)

You can pass a list[int] or a tuple[str, ...] because Sequence is covariant. If you changed the parameter to list[object], a list[int] would be rejected by mypy because list is invariant. This is why Sequence is often the better choice for read-only generic parameters.

On the other hand, if your function needs to append to a list, the parameter must be list[T] and the invariance is correct: you cannot safely append an object to a list[int].

Runtime Behavior and Performance Considerations

Type hints have no runtime effect on sequence operations. They are not enforced by the Python interpreter; they are only used by static checkers and IDEs. However, the choice of hint can influence runtime behavior indirectly if you use a library like pydantic or dataclasses that inspects annotations. For example, pydantic may try to coerce input to the annotated type. If you annotate a field as Sequence[int], pydantic might not know how to instantiate a generic Sequence and may raise an error. In such cases, you need to use a concrete type like list[int] or tuple[int, ...].

From a performance perspective, there is no overhead from using Sequence over list in a type hint. The annotation is not executed at runtime unless you access __annotations__. The real performance concern is whether you convert data unnecessarily. If a function accepts Sequence but you convert a tuple to a list just to satisfy a list hint, that conversion costs time and memory. Choosing the right hint avoids that conversion.

Compatibility with Older Python Versions

The typing.Sequence class has been available since Python 3.5. In Python 3.9 and later, you can use the built-in collections.abc.Sequence as a type hint without importing from typing. For example:

from collections.abc import Sequence def total(values: Sequence[float]) -> float: return sum(values)

This is equivalent to the typing.Sequence version and is preferred in modern code. If you need to support Python 3.8 or earlier, stick with typing.Sequence. Also note that the list[int] syntax for built-in generics is only available from Python 3.9; before that you must use typing.List[int]. If your project targets older versions, use the typing forms consistently.

When you use Sequence in a type alias or a function signature, static checkers treat it as an abstract type. This means you cannot instantiate it directly. The hint is only for annotation, not for creating objects. If you need a default value like an empty sequence, use () or [] with a cast, or use a factory function that returns the appropriate concrete type.

A final note on maintainability: using Sequence for read-only parameters makes your code more flexible and reduces the need for callers to know the exact collection type. It also signals that your function does not mutate its input, which is valuable documentation. For return types, prefer concrete types so callers can rely on specific methods. This balance keeps the type hints accurate and the API clear.

python sequence type hint: Practical Usage and Code Examples | RYUSLOG DEV