Back to Blog
Python

Python Sequence Pattern Matching

python sequence pattern: Learn how to use Python sequence patterns in match statements, including star patterns, guards, and making custom sequence types matchable.

pattern matchingmatch statementsequencestar patterncustom sequence
Illustration of Python sequence pattern matching showing a list being decomposed into its elements with a match arrow.

python sequence pattern requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

Python's structural pattern matching, introduced in version 3.10, provides a match statement that can decompose data based on its shape. Among the pattern types, the sequence pattern is one of the most commonly used because it directly handles lists, tuples, and other sequence-like objects. This article explains how sequence patterns work, how to combine them with star patterns and guards, and how to make your own sequence classes participate in matching.

The Basics of Sequence Patterns in match Statements

A sequence pattern matches a subject against a fixed-length sequence of subpatterns. The simplest form uses square brackets or parentheses to denote the expected structure:

point = (3, 4) match point: case (0, 0): print("Origin") case (x, y): print(f"Coordinates: {x}, {y}")

Here, case (0, 0) matches a two-element sequence where both elements are zero. The second case captures the two elements into variables x and y. The pattern works with any object that supports len() and indexing, so both lists and tuples are valid subjects.

You can use either [] or () in the pattern; they behave identically. The pattern case [a, b] is equivalent to case (a, b). This flexibility lets you write patterns that visually match the data you expect.

Sequence patterns check the length first. If the subject does not have the expected number of elements, the pattern fails and the next case is tried. This length check is strict: case [a, b] only matches a sequence of exactly two elements.

Matching Variable-Length Sequences with Star Patterns

Real-world data often has a variable number of elements. A star pattern (*) captures zero or more elements into a list. This is useful for matching the head and tail of a sequence, or for ignoring a middle section.

def describe(seq): match seq: case []: return "empty" case [first]: return f"single item: {first}" case [first, *rest]: return f"first: {first}, rest count: {len(rest)}"

The star pattern can appear at most once per sequence pattern. It can be placed at the beginning, middle, or end:

case [*prefix, last]: # matches at least one element case [first, *middle, last]: # matches at least two elements

When the star pattern is used alone, as in case [*all], it matches any sequence and captures all elements into all. This is rarely useful because the same result can be achieved with a variable pattern, but it can be combined with a guard to filter by length.

A common mistake is to use a star pattern with a generator or an iterator. Sequence patterns require an object that supports len() and indexing; generators do not. If you need to match an iterator, convert it to a list first, or use an OR pattern with explicit checks.

Using Guards to Constrain Sequence Matches

A guard is an if clause attached to a case that adds an additional condition beyond the pattern itself. Guards are evaluated only after the pattern matches, so you can safely reference captured variables.

match numbers: case [a, b] if a < b: print(f"Increasing pair: {a}, {b}") case [a, b]: print(f"Non-increasing pair: {a}, {b}")

Guards are especially useful when a pattern is too broad on its own. For example, you might want to match a sequence of exactly two numbers where the second is double the first:

case [x, y] if y == 2 * x: print(f"Doubled: {x} -> {y}")

Keep guards simple. Complex logic inside a guard can make the match block harder to read. If the condition is involved, consider extracting it into a function and calling that function in the guard.

Nested Sequence Patterns and Subpatterns

Sequence patterns can be nested inside each other, allowing you to match deeply structured data in one case. This is common when working with parsed input, configuration trees, or recursive data structures.

match command: case ["move", [x, y]]: print(f"Move to ({x}, {y})") case ["draw", [start, end]]: print(f"Draw from {start} to {end}") case ["quit"]: print("Goodbye")

Each nested sequence pattern follows the same rules: it checks length and then attempts to match its own subpatterns. You can combine nested patterns with star patterns and guards at any level.

Nested patterns also work with other pattern types, such as mapping patterns or class patterns. For instance, case [{"type": "circle", "radius": r}] matches a list containing a dictionary with those keys.

Making Custom Sequence Types Work with Pattern Matching

Sequence patterns are not limited to built-in lists and tuples. Any class that implements the sequence protocol—__len__ and __getitem__—can be matched. The match statement uses these methods to determine the length and to fetch individual elements.

Consider a custom Range class that behaves like a sequence:

class Range: def __init__(self, start, end): self._values = list(range(start, end)) def __len__(self): return len(self._values) def __getitem__(self, index): return self._values[index]

You can now use this class in a sequence pattern:

r = Range(0, 3) match r: case [0, 1, 2]: print("Matches exactly") case [first, *rest]: print(f"First: {first}, rest: {rest}")

Because Range supports indexing and length, the pattern works as expected. If your class does not implement these methods, the pattern will raise a TypeError when the match statement attempts to inspect it.

A subtle point: the sequence pattern does not require the subject to be an instance of collections.abc.Sequence. It only needs the two methods. This means you can match objects that are not formally registered as sequences, as long as they behave like one.

Performance and Runtime Behavior of Sequence Matching

Sequence patterns perform a length check and then index into the subject for each element in the pattern. For a pattern with n subpatterns, the runtime cost is O(n) in the number of elements accessed, plus the cost of the length check. For most patterns, n is small, so the overhead is negligible.

However, be aware that indexing a custom sequence may have side effects or be expensive if the __getitem__ implementation does heavy work. The match statement will call __getitem__ once for each position in the pattern, in order, until the pattern fails or succeeds. If your sequence is lazy or computes values on the fly, this can trigger unexpected computation.

Another performance consideration is the order of case clauses. The match statement evaluates cases in order, so put the most specific or most common patterns first. A broad pattern with a star that appears early will prevent later, more specific patterns from being reached.

For large sequences, the length check is O(1) if __len__ is O(1). Most built-in sequences have constant-time length, but a custom class might not. If your __len__ iterates the entire underlying data, every pattern match will incur that cost.

Common Pitfalls and Edge Cases

Several behaviors can surprise developers new to sequence patterns. One is matching strings. A string is a sequence of characters, so case [a, b] will match a two-character string and bind a and b to the individual characters. This is often not what you want. If you need to match a string as a single value, use a literal pattern or a class pattern like case str().

Another pitfall is matching dictionaries. Dictionaries are not sequences; they do not support integer indexing. Using a sequence pattern on a dictionary will raise a TypeError. Use a mapping pattern instead: case {"key": value}.

A related issue is matching objects that support indexing but not length. The sequence pattern requires both __len__ and __getitem__. If a class implements only __getitem__, the pattern will fail with a TypeError. This is a common mistake when trying to match custom iterable classes that only implement __iter__.

Finally, remember that the star pattern captures a list, not a tuple. Even if the subject is a tuple, the captured rest variable will be a list. This is consistent with the behavior of *args in function calls, but it can be surprising if you expect the same type as the original sequence.

Understanding these edge cases helps you write robust match statements that behave predictably across different sequence types.

python sequence pattern: Practical Usage and Code Examples | RYUSLOG DEV