Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pattern matching

match evaluates its scrutinee once and selects a case. The scrutinee is an enum, an anonymous sum, a class, a tuple, an integer, a str, or a bool, or a reference to one:

enum Reading:
    Missing
    Value(i64)

def amount(reading: Reading) -> i64:
    match reading:
        case Reading.Missing:
            0
        case Reading.Value(number):
            number

A case holds one pattern. A pattern is one of:

PatternMatches
_any value, binding nothing
nameany payload, bound to an immutable local
_ as namethe whole matched value, bound to an immutable local
name: MemberTypea whole member of an anonymous sum; see member patterns
Enum.Variant or Enum.Variant(p, ...)that variant, with one pattern per payload position
Some(p), Nothing, Ok(p), Err(p)that standard variant
(p, q) or (p,)a tuple with one pattern per component
Class(p, ..., field=p, ...)a class value, with one pattern per field; see class patterns
1, -1, 0u8an integer equal to the literal
"text"a str equal to the literal
True, Falsethat bool
p | qa value either alternative matches

A user-defined or library variant is always qualified by its enum’s name, a module path to it, or an alias of the matched type. Some, Nothing, Ok, and Err are never qualified. A pattern takes no type arguments, because the matched value supplies them: case Choice.Value(v) matches a Choice[i64] and a Choice[str] alike, and case Choice[i64].Value(v) is an error.

At an anonymous-sum position, an enum variant pattern selects that enum member before testing its variant. This also works in nested payloads, such as Err(ParseError.Invalid) for Result[T, AllocError | ParseError]. Coverage includes every variant of every member. A generic qualifier that names several members is ambiguous; select one with a typed member pattern before matching its variants. See anonymous sums.

A bare name binds only at a payload or field position; the case’s own pattern is a variant, a class or tuple pattern, a literal, _, alternatives of those, _ as name, or a typed member pattern. A bare name as the case’s own pattern is an error, and a payload name equal to a variant of that payload’s enum is rejected, so a forgotten qualifier cannot silently bind everything. A variant pattern at a Box[T] payload matches the box’s contents. Patterns nest to at most 64 levels.

enum Shape:
    Dot
    Circle(i64)
    Rect(i64, i64)

def size(shape: Option[Shape]) -> i64:
    match shape:
        case Some(Shape.Circle(side) | Shape.Rect(side, _)):
            side
        case Some(Shape.Dot) | Nothing:
            0

A literal pattern matches an integer, str, or bool position: the scrutinee itself, a payload, or the contents of a Box. An unsuffixed integer literal takes the position’s type and must fit it; a suffixed one must name that type exactly. Float literals are not patterns.

Tuple patterns

Tuple patterns are recursive fixed-arity products. (p,) matches a singleton; (p) groups a pattern. Empty () is unit, not an empty tuple pattern; use _ for a unit component. A tuple pattern can appear at the root or inside enum, class, tuple, or boxed payloads. Every component must have a pattern. At an anonymous-sum position, it selects a unique tuple member with that arity. If several members have the same arity, bind one with a typed member pattern before matching its components.

def combine(left: Option[i64], right: Option[i64]) -> Result[i64, OverflowError[i64]]:
    match (left, right):
        case (Some(a), Some(b)):
            Ok(a + b)
        case (Some(a), Nothing):
            Ok(a)
        case (Nothing, Some(b)):
            Ok(b)
        case (Nothing, Nothing):
            Ok(0)

def main() -> Result[(), IoError | OverflowError[i64]]:
    print(combine(Some(2), Some(3))?)?
    Ok(())
5

Coverage considers combinations of component patterns, so omitting (Nothing, Nothing) above is an error. Alternatives retain the existing 256-combination expansion limit and decision-tree work limit. Matching owns and moves each bound affine component once and destroys ignored components once. Matching a reference instead binds component references, preserving disjoint component loans.

Class patterns

A class pattern takes a class value apart, written as the class’s generated constructor takes its fields: positional patterns in field-declaration order, then field=pattern. It matches a class position, the contents of a Box, or that class’s member of an anonymous sum.

class Point:
    x: i64
    y: i64

def axis(p: Point) -> Result[i64, OverflowError[i64]]:
    match p:
        case Point(0, y):
            Ok(y)
        case Point(x=x, y=0):
            Ok(x)
        case Point(x, y):
            Ok(x + y)

The rules have no exceptions:

  • Every field appears exactly once; ignore one with _ or field=_. A class with no fields is written Name().
  • Positional patterns precede named ones. An unknown, repeated, or missing field, or one positional pattern too many, is an error.
  • The class is named as in an expression: by its name, a module path, or an import. As for enums, the matched value supplies type arguments, so case Wrap(item) matches a Wrap[i64] and case Wrap[i64](item) is an error.
  • Each field must be visible where the pattern is written, so a class with a private field cannot be taken apart outside its module.
  • A class pattern tests nothing itself: it is irrefutable when every subpattern is, and a nested literal or variant makes it refutable.
  • A bare name that spells the class at its position, such as case Err(OverflowError):, is rejected rather than bound, so the parentheses cannot be forgotten silently.
  • Owned matching moves bound fields into their bindings and destroys the rest, as for an enum payload. A class with __del__ expects every field initialized, so an owned match cannot bind its fields; match a reference such as &value to borrow them, or bind it whole. Borrowed matching binds field references, as for payloads.

A @newtype class or any one-field class is matched the same way; its pattern costs no projection at run time. The error of a checked: block is the motivating case:

def total(a: i64, b: i64) -> i64:
    result = checked:
        a + b
    match result:
        case Ok(sum):
            sum
        case Err(OverflowError(wrapped)):
            wrapped
def opcode(name: str) -> i64:
    match name:
        case "cpy":
            1
        case "inc" | "dec":
            2
        case _:
            0

def describe(reading: Option[i64]) -> str:
    match reading:
        case Some(0):
            "zero"
        case Some(_):
            "nonzero"
        case Nothing:
            "missing"

case _ as name: matches every value the earlier cases leave and binds the whole matched value, for any supported scrutinee type. It does not make floats, collections, or other unsupported scrutinees matchable. It is accepted only as a case’s entire pattern: not as a | alternative, and not at a payload position, where the name alone binds. Because it matches everything, a case after it is unreachable, and it is itself unreachable when earlier cases are exhaustive. name cannot be a variant of the matched enum, _, or mut.

def forward(shape: Shape) -> i64:
    match shape:
        case Shape.Dot:
            0
        case _ as other:
            size(Some(other))

Every alternative of p | q binds the same names with the same types, at any depth. Cases are tried in order and the first match runs. Bindings stay within the arm and may shadow outer names.

Coverage must be exhaustive; the error lists missing values as patterns, such as missing Ok(Some(Shape.Dot)), Ok(Nothing) or missing Point(_, _). A case, or a single alternative, that earlier patterns already cover is an error. Incorrect payload arity and wrong enum identities are errors. A pattern expands to at most 256 combinations of its alternatives.

Literals never cover an integer or str position: such a position also needs _ or a payload name, and a match on an integer or str ends with case _: or case _ as name:. True and False together cover a bool, so a _ after both is unreachable. A repeated literal is an unreachable case.

Matching preserves source-order behavior, including overlapping payload patterns. Native dispatch uses a jump table for at least 256 distinct integer values or enum tags forming one contiguous range, in any source order. Signed values and all integer widths are supported; out-of-range values still take the wildcard or defensive invalid-tag trap. Table storage grows with the case count. Smaller or sparse sets use comparison chains; a str case compares lengths and then bytes. These chains can be sensitive to which cases occur most often; the dispatch measurements explain the conservative table threshold. A match on at least 16 distinct str literals instead binary-searches them once, so its cost grows with the logarithm of the case count and does not depend on arm order; see the string match measurements. Keep using match for clear exhaustive branching; dispatch selection does not change its behavior.

Guards, float patterns, range patterns, and pattern as name for any pattern other than the case’s own _ are unsupported. A scrutinee of any other type, such as a float or a collection, is rejected with advice to use an if/elif chain.

Payload and whole-value bindings cannot be declared mut. To change an owned payload, move it into a mutable local inside the arm (mut local = payload); to change it in place, match &mut value.

Continuing arms must agree on their result type. Arms ending in return, break, or continue contribute no join value. A function-tail match supplies the final expression of its selected arm, just like a tail conditional. A match elsewhere in a suite is a statement, and its arms’ values are discarded, unless it is a statement’s value.

A match as a statement’s value

A match block can be the whole value of these statements, and of no other position:

StatementEffect
name = match value:, with mut or an annotation as for any bindingdeclares the local, or assigns an existing mut local
place = match value:, where place is a field, a collection element, or *referenceassigns the place
return match value:returns from the function
def area(shape: Shape) -> Result[i64, OverflowError[i64]]:
    side: i64 = match shape:
        case Shape.Dot:
            return Ok(0)
        case Shape.Circle(side):
            side
        case Shape.Rect(width, height):
            if width < height:
                width
            else:
                height
    Ok(side * side)

The statement ends with the last case; nothing follows the block on its line. Patterns, exhaustiveness, and ownership of the scrutinee are those of any other match.

In a binding or assignment, the final expression of the selected case is the value. A final if or match supplies the value of its selected suite.

  • Continuing cases must agree on one type. An annotation, or the type of the assigned local, is the expected type of every case’s final expression, as it is for an ordinary right-hand side; without one, each case is inferred alone, so Nothing or []? needs the annotation.
  • A case that ends in return, break, continue, or a propagating ? gives no value. At least one case must continue; a match whose cases all exit is rejected with advice to write it as a statement.
  • A case whose final statement has no value, such as pass or an assignment, makes the match unit, which cannot be bound.
  • The value cannot be a reference or a closure with borrowed captures, because the case’s bindings end before the statement uses it. Produce an owned value, or declare mut name and assign in each case.
  • Earlier expressions in a case are discarded as in any suite, so an unhandled Result there is still an error.

Evaluation order is: the scrutinee; the selected case’s suite; cleanup of that case’s bindings and locals, in reverse declaration order, and of scrutinee parts no pattern bound; then the binding or assignment. An assignment evaluates destination indices and destroys the replaced value after all of that. An exit from a case performs the ordinary cleanup for that exit and assigns nothing.

return match value: returns the final expression of the selected case, exactly as if that case ended with return expression. Every case therefore exits, a call there is a source-tail call, and a statement after the block is unreachable. A case that ends without a value returns unit, which is an error unless the function returns unit. A returned reference follows the rules of any other returned reference.

Augmented assignment, assignment to several targets, call arguments, operands, and every other expression position reject a match block; bind its value to a name first. A block if supports the same whole-statement value positions and retains these expression-position restrictions.

Ownership

Owned matching consumes the scrutinee: bound payloads move into their bindings and every part no pattern binds is destroyed. _ as name moves the whole value into name, with no copy or allocation. Testing a literal copies the integer, str, or bool it compares, so a later pattern can still bind or move the value; for a str the copy shares the text and allocates nothing. Matching a shared or exclusive reference binds shared or exclusive payload references instead; see borrowed matching. Unit payload bindings produce unit expressions and can be returned from a unit-returning function.