Pythonのmatch case文とは、値の「形」と「中身」を上から順にパターンと照らし合わせ、最初に当てはまったcaseのブロックだけを実行する構文です(Python 3.10以降)。 ほかの言語のswitch文の代わりとして使えるだけでなく、リストの長さ・辞書のキー・クラスの属性を調べながら、同時に中の値を変数へ取り出せるところが大きな特徴です。

この記事では、全パターンを「書き方・マッチする値・実行結果」の早見表にまとめたうえで、定数名を書いたつもりが全ての値にマッチしてしまう問題、Python 3.9以前で動かしたときのエラー、辞書は部分一致・文字列はシーケンス扱いされないといった注意点を、すべて実際に動かした出力つきで説明します。if-elifや辞書ディスパッチとの書き比べ、JSON風データの分解、dataclassと__match_args__の例もそのままコピーして試せます。

確認環境:Python 3.11.15(Linux)。3.9のエラーは Python 3.9.23 で確認しました。仕様は公式ドキュメント(言語リファレンスの「match 文」、3.14版)を開いて照合しています。

match caseの早見表(全パターン)

次の表の「実行結果」は、あとで載せる確認用コード(全パターンを1つの関数にまとめたもの)にその値を渡したときの実際の出力です。

パターン 書き方 マッチする値 渡した値 → 実行結果
リテラル case 0: case "ok": case None: == で等しい値(None・True・False は is で比較) 0 → リテラル: 0 / "ok" → リテラル: 文字列ok
OR case 200 | 201: どれか1つに当てはまる値 201 → OR: 200か201
シーケンス(固定長) case [x, y]: 長さ2のリスト・タプルなど [1, 2] も (1, 2) も → x=1 y=2
シーケンス(可変長) case [first, *rest]: 長さ1以上のシーケンス [1, 2, 3] → 先頭=1 残り=[2, 3]
辞書(マッピング) case {"type": "user", "name": name}: 書いたキーを全て持つ辞書(ほかのキーがあってもよい) {"type": "user", "name": "sato", "age": 30} → name=sato
クラス case Point(x=0, y=y): Point のインスタンスで x が 0 Point(0, 5) → y=5
ガード case パターン if 条件: パターンに当てはまり、かつ条件が真 5000 → 1000より大きい数 5000
as case int() | float() as n: パターンに当てはまった値全体を n に入れる 同上(n に 5000 が入る)
キャプチャ case x: 何にでもマッチし、x に値を入れる 最後の case にだけ書ける
ワイルドカード case _: 何にでもマッチ(変数には入れない) [] → どれにも当てはまらない
値(ドット付きの名前) case Code.OK: その定数と == で等しい値 200 → OK

迷いやすい点を先にまとめると次の4つです。

気をつける点 何が起きるか こう書くとよい
case NOT_FOUND: のように名前だけを書く 定数との比較にならず、何にでもマッチする(キャプチャ) Code.NOT_FOUND のようにドット付きで書く
辞書パターン 書いたキー以外があってもマッチする(部分一致) 厳密にしたいときは **rest とガードで確認
文字列をシーケンスパターンに渡す "ab" は [a, b] にマッチしない list("ab") にしてから渡す
Python 3.9 以前で実行 SyntaxError: invalid syntax で止まる 3.10 以上で動かす(または if-elif で書く)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python match caseの使い方|パターン全種の早見表と注意点」

まず全パターンを1つのコードで動かす

早見表の元になった確認用コードです。Python 3.10以上があれば、このまま保存して実行できます。

# t1.py
from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

def check(v):
    match v:
        case 0:
            return "リテラル: 0"
        case 200 | 201:
            return "OR: 200か201"
        case "ok":
            return "リテラル: 文字列ok"
        case None:
            return "リテラル: None"
        case [x, y]:
            return f"シーケンス: 2要素 x={x} y={y}"
        case [first, *rest]:
            return f"シーケンス: 先頭={first} 残り={rest}"
        case {"type": "user", "name": name}:
            return f"辞書: name={name}"
        case Point(x=0, y=y):
            return f"クラス: x=0 の Point, y={y}"
        case int() | float() as n if n > 1000:
            return f"ガード+as: 1000より大きい数 {n}"
        case str() as s:
            return f"クラス+as: 文字列 {s!r}"
        case _:
            return "ワイルドカード: どれにも当てはまらない"

for v in [0, 201, "ok", None, [1, 2], (1, 2), [1, 2, 3], [],
          {"type": "user", "name": "sato", "age": 30}, {"type": "admin"},
          Point(0, 5), Point(1, 5), 5000, 3.5, "abc", True, 1.0-1.0]:
    print(f"{v!r:45} -> {check(v)}")

実行結果(Python 3.11.15):

0                                             -> リテラル: 0
201                                           -> OR: 200か201
'ok'                                          -> リテラル: 文字列ok
None                                          -> リテラル: None
[1, 2]                                        -> シーケンス: 2要素 x=1 y=2
(1, 2)                                        -> シーケンス: 2要素 x=1 y=2
[1, 2, 3]                                     -> シーケンス: 先頭=1 残り=[2, 3]
[]                                            -> ワイルドカード: どれにも当てはまらない
{'type': 'user', 'name': 'sato', 'age': 30}   -> 辞書: name=sato
{'type': 'admin'}                             -> ワイルドカード: どれにも当てはまらない
Point(x=0, y=5)                               -> クラス: x=0 の Point, y=5
Point(x=1, y=5)                               -> ワイルドカード: どれにも当てはまらない
5000                                          -> ガード+as: 1000より大きい数 5000
3.5                                           -> ワイルドカード: どれにも当てはまらない
'abc'                                         -> クラス+as: 文字列 'abc'
True                                          -> ワイルドカード: どれにも当てはまらない
0.0                                           -> リテラル: 0

出力から読み取れるポイントは次のとおりです。

  • リストとタプルは区別されません。 case [x, y] は [1, 2] にも (1, 2) にもマッチします。case (x, y) と書いても同じ意味です。
  • 辞書は部分一致です。 "age" というキーが余分にあってもマッチしています。
  • 空のリスト [] は [first, *rest] にマッチしません。 first に入る要素が必要だからです。
  • 最後の行の 0.0 は case 0 にマッチしています。 リテラルは == で比べるため、0 == 0.0 が真になるからです。
  • 上から順に試し、最初に当てはまったcaseだけが実行されます。 switch文のような「次のcaseへ流れ落ちる」動きはなく、breakも要りません。

match文の基本の形

match 調べたい値:
    case パターン1:
        処理1
    case パターン2 if 条件:
        処理2
    case _:
        どれにも当てはまらないときの処理

「調べたい値」のことを公式ドキュメントではサブジェクトと呼びます。どのcaseにも当てはまらず、case _: も無いときは、何も実行されずに次の行へ進みます(エラーにはなりません)。入力の取りこぼしに気づけるよう、最後に case _: を置いて、例外を投げるかログを出しておくと安心です。

パターン別の書き方と実行結果

リテラルとOR:決まった値で分ける

switch文の代わりとして最もよく使う形です。数値・文字列・None・True・False が書けます。負の数(-1)、複素数(1+2j)、隣り合わせに並べた文字列("a" "b")も書けることを実行で確かめました。

def label(status):
    match status:
        case 200 | 201:
            return "成功"
        case 404:
            return "見つかりません"
        case 500 | 502 | 503:
            return "サーバー側のエラー"
        case _:
            return "その他"

f文字列や計算式は書けません。 実際に試すと次のエラーになります。

書いたcase エラーの文面(Python 3.11.15)
case f"{x}": SyntaxError: patterns may only match literals and attribute lookups
case 1 + 1: SyntaxError: imaginary number required in complex literal

2つめの文面が少しわかりにくいのは、1 + 2j のような複素数だけが「足し算の形」として認められているためです。計算した値と比べたいときは、ガード(case x if x == a + b:)を使います。

シーケンス:リストやタプルの形で分ける

長さと並びを同時に調べ、要素を変数に取り出せます。

def kind(v):
    match v:
        case [a, b]:
            return f"2要素のシーケンス a={a!r} b={b!r}"
        case str():
            return "文字列"
        case _:
            return "その他"

for v in ["ab", b"ab", bytearray(b"ab"), ["a", "b"], ("a", "b"), range(2),
          {"a", "b"}, {"a": 1, "b": 2}, iter([1, 2])]:
    print(f"{v!r:22} -> {kind(v)}")

実行結果:

'ab'                   -> 文字列
b'ab'                  -> その他
bytearray(b'ab')       -> その他
['a', 'b']             -> 2要素のシーケンス a='a' b='b'
('a', 'b')             -> 2要素のシーケンス a='a' b='b'
range(0, 2)            -> 2要素のシーケンス a=0 b=1
{'a', 'b'}             -> その他
{'a': 1, 'b': 2}       -> その他
<list_iterator object at 0x7fa040503f70> -> その他

文字列・bytes・bytearray は、長さが合っていてもシーケンスパターンにマッチしません。 公式ドキュメントにも、サブジェクトが str・bytes・bytearray のインスタンスならシーケンスパターンは成功しない、と書かれています。"ab" が1文字ずつに分かれて ["a", "b"] と同じ扱いになると、リストを期待した分岐に文字列が紛れ込んでしまうため、この仕様はむしろ助けになります。

渡す値 case [a, b] にマッチするか
list・tuple・range(長さ2) する
str・bytes・bytearray しない
set・dict しない(順序のあるシーケンスではない)
イテレータ・ジェネレータ しない(list() にしてから渡す)

文字列を1文字ずつ分けたいときは、match list("ab"): のように自分でリストへ変換します(a b と取り出せることを確認済み)。

* を使うと残りをまとめて受け取れます。case [first, *rest]、case [*head, last]、case [first, *_, last](途中は捨てる)のように書けます。* を付けられるのは1つのパターンに1回だけで、case [*a, *b] は「SyntaxError: multiple starred names in sequence pattern」になります。

辞書(マッピング):キーで分ける。部分一致に注意

辞書パターンは、書いたキーが全て存在し、値がそれぞれのパターンに合えばマッチします。書いていないキーは無視されます。

def kind(d):
    match d:
        case {"id": 1}:
            return "id が 1(ほかのキーがあってもマッチ)"
        case {"id": _, **rest} if not rest:
            return "キーが id だけ"
        case {"id": _, **rest}:
            return f"id とそれ以外: {rest}"
        case {}:
            return "辞書なら何でも(空でなくてもマッチ)"
        case _:
            return "辞書ではない"

for d in [{"id": 1, "name": "a"}, {"id": 2}, {"id": 3, "tag": "x"},
          {"name": "b"}, {}, [("id", 1)]]:
    print(f"{d!r:26} -> {kind(d)}")

実行結果:

{'id': 1, 'name': 'a'}     -> id が 1(ほかのキーがあってもマッチ)
{'id': 2}                  -> キーが id だけ
{'id': 3, 'tag': 'x'}      -> id とそれ以外: {'tag': 'x'}
{'name': 'b'}              -> 辞書なら何でも(空でなくてもマッチ)
{}                         -> 辞書なら何でも(空でなくてもマッチ)
[('id', 1)]                -> 辞書ではない
  • case {}: は「空の辞書」ではなく「辞書なら何でも」にマッチします。 空の辞書だけを選びたいときは case {**rest} if not rest: か、case dict() as d if not d: と書きます。
  • キーをちょうどその組み合わせに限定したいときは、**rest で残りを受け取り、ガードで not rest を確かめます。
  • **_ とは書けません(「SyntaxError: invalid syntax」)。書かなくても余分なキーは無視されるためです。
  • シーケンスの [] は「長さ0のシーケンスだけ」にマッチするので、[] と {} では意味が違う点を覚えておくと混乱しません。

クラス:型と属性で分ける

クラス名(属性=パターン) と書くと、「そのクラスのインスタンスか」と「属性の値」を同時に調べられます。

case Point(x=0, y=y):      # Point で x が 0。y は変数 y に取り出す
case int():                # int のインスタンス(かっこが必要)
case str() as s:           # 文字列ならそのまま s に入れる
case int(n):               # int なら値そのものを n に入れる

かっこを忘れて case int: と書くと、型のチェックではなくキャプチャになります(int という名前の変数に値を入れるだけ)。型で分けたいときは必ず int() と書きます。

int(n) のように位置で書いたときに「値そのもの」が入るのは、公式ドキュメントに挙げられている組み込み型(bool・bytearray・bytes・dict・float・frozenset・int・list・set・str・tuple)だけです。自作クラスで位置の書き方を使うには、後述の __match_args__ が必要です。

ガード:パターンに条件を足す

case パターン if 条件: と書くと、パターンに当てはまったうえで条件が真のときだけ、そのcaseが選ばれます。条件が偽なら次のcaseへ進みます。

match [1, 2]:
    case [a, b] if a > 10:
        print("big")
    case _:
        print("a, b =", a, b)

実行結果:

a, b = 1 2

ガードが偽で次のcaseへ進んでも、取り出した変数 a と b には値が入ったままです。match文は新しいスコープを作らないので、キャプチャした変数はmatch文の後でも使えます。同じ名前の変数を前後で使っている場合は上書きされる点に気をつけます。

asとキャプチャ:当てはまった値を変数に入れる

match ("error", 500):
    case ("error" | "warn" as level, int(code)) as whole:
        print(level, code, whole)

実行結果:

error 500 ('error', 500)

as は、ORパターンやクラスパターンに当てはまった値に名前を付けるときに使います。パターンの一部にも全体にも付けられます。

ORパターンで変数を取り出すときは、全ての選択肢が同じ名前を取り出す必要があります。case [x] | [y]: は「SyntaxError: alternative patterns bind different names」、同じ名前を2回使う case [a, a]: は「SyntaxError: multiple assignments to name 'a' in pattern」になります。「2つの要素が同じ」を調べたいときは case [a, b] if a == b: と書きます。

定数名を書くと全てにマッチする問題と直し方

match文でいちばん気づきにくいのがこの点です。ドットの付かない名前は、定数との比較ではなく「その名前の変数に値を入れる」キャプチャとして扱われます。

うまくいかない例1:SyntaxErrorで止まる

# t2.py
NOT_FOUND = 404

def status(code):
    match code:
        case NOT_FOUND:
            return "見つかりません"
        case _:
            return "その他"

print(status(200))

実行結果(Python 3.11.15。パスは短くしています):

  File "t2.py", line 5
    case NOT_FOUND:
         ^^^^^^^^^
SyntaxError: name capture 'NOT_FOUND' makes remaining patterns unreachable

「NOT_FOUND という名前のキャプチャが、残りのパターンを到達不能にしている」という意味です。後ろに別のcaseがある場合は、Pythonがこのように教えてくれます。 同じ理由で、case _: の後ろに別のcaseを書くと「SyntaxError: wildcard makes remaining patterns unreachable」になります。

うまくいかない例2:エラーにならず全てにマッチする

後ろにcaseが無いと、エラーにはならず、そのまま動いてしまいます。

# t3.py
NOT_FOUND = 404

def status(code):
    match code:
        case NOT_FOUND:
            return f"NOT_FOUND に入った値: {NOT_FOUND}"

print(status(200))
print(status("何でも"))

実行結果:

NOT_FOUND に入った値: 200
NOT_FOUND に入った値: 何でも

200 でも文字列でもマッチし、関数の中では NOT_FOUND という名前のローカル変数に渡した値が入っています。モジュールの定数 NOT_FOUND(404)は書き換わりませんが、比較のつもりで書いたcaseが、実際には何でも受け取ることになります。

直し方:ドット付きの名前(値パターン)で書く

ドットを含む名前は「値パターン」として扱われ、== で比較されます。 Enum、クラスの属性、モジュールの属性(http.HTTPStatus.NOT_FOUND など)が使えます。

# t4.py
from enum import IntEnum
from http import HTTPStatus

class Code(IntEnum):
    OK = 200
    NOT_FOUND = 404

class Const:
    MAX = 100

def status(code):
    match code:
        case Code.OK:
            return "OK"
        case HTTPStatus.NOT_FOUND:
            return "見つかりません"
        case Const.MAX:
            return "上限"
        case _:
            return "その他"

for c in [200, 404, 100, 500]:
    print(c, status(c))

実行結果:

200 OK
404 見つかりません
100 上限
500 その他
定数の置き場所 caseの書き方 扱い
モジュール直下の変数 NOT_FOUND = 404 case NOT_FOUND: キャプチャ(何にでもマッチ)
Enum のメンバー case Code.NOT_FOUND: 値パターン(比較)
クラスの属性 case Const.MAX: 値パターン(比較)
別モジュールの定数 case settings.MAX: 値パターン(比較)
どうしても名前だけで書きたい case x if x == NOT_FOUND: ガードで比較
値を直接書く case 404: リテラル(比較)

定数はEnumにまとめておくと、match文でそのまま使えて読みやすくなります。

Python 3.9以前で動かしたときのエラー

match文はPython 3.10で加わった構文です。先ほどの t1.py を Python 3.9.23 で実行すると、次のようになりました(パスは短くしています)。

  File "t1.py", line 9
    match v:
          ^
SyntaxError: invalid syntax

「match文は使えません」とは言ってくれず、match v: の行で SyntaxError: invalid syntax とだけ表示されます。 構文エラーなので、そのファイルは1行も実行されません(match文より前に書いた print も出ません)。

状況 確認・対応
手元では動くのにサーバーで invalid syntax python3 --version で両方のバージョンを確かめる
3.9以前でも動かす必要がある if-elif と isinstance で書く
配布するパッケージで使う pyproject.toml の requires-python = ">=3.10" を指定する

match と case は「ソフトキーワード」で、match文の位置に書かれたときだけ特別な意味を持ちます。そのため match = "..." や re.match(...) のように、変数名・関数名としての match は3.10以降でもそのまま使えます(match = "変数名として使える" と代入して print できることを確認しました)。

if-elif・辞書ディスパッチとの書き比べ

「コマンド文字列を解釈する」という同じ処理を、3通りで書いて比べます。

# u3.py
def by_if(cmd):
    parts = cmd.split()
    if len(parts) == 1 and parts[0] in ("quit", "exit"):
        return "終了"
    elif len(parts) == 2 and parts[0] == "go":
        return f"{parts[1]} へ移動"
    elif len(parts) >= 2 and parts[0] == "take":
        return f"{'と'.join(parts[1:])} を取る"
    else:
        return f"不明なコマンド: {cmd}"

def by_match(cmd):
    match cmd.split():
        case ["quit" | "exit"]:
            return "終了"
        case ["go", place]:
            return f"{place} へ移動"
        case ["take", *things] if things:
            return f"{'と'.join(things)} を取る"
        case _:
            return f"不明なコマンド: {cmd}"

HANDLERS = {
    "quit": lambda args: "終了",
    "exit": lambda args: "終了",
    "go": lambda args: f"{args[0]} へ移動",
    "take": lambda args: f"{'と'.join(args)} を取る",
}

def by_dict(cmd):
    name, *args = cmd.split()
    handler = HANDLERS.get(name)
    if handler is None:
        return f"不明なコマンド: {cmd}"
    return handler(args)

for c in ["quit", "go north", "take pen note", "jump", "go"]:
    try:
        d = by_dict(c)
    except Exception as e:
        d = f"{type(e).__name__}: {e}"
    print(f"{c!r:16} if: {by_if(c)} / match: {by_match(c)} / dict: {d}")

実行結果:

'quit'           if: 終了 / match: 終了 / dict: 終了
'go north'       if: north へ移動 / match: north へ移動 / dict: north へ移動
'take pen note'  if: penとnote を取る / match: penとnote を取る / dict: penとnote を取る
'jump'           if: 不明なコマンド: jump / match: 不明なコマンド: jump / dict: 不明なコマンド: jump
'go'             if: 不明なコマンド: go / match: 不明なコマンド: go / dict: IndexError: list index out of range

最後の行に違いが出ています。引数なしの go を渡したとき、match文は「長さ2のリスト」という形まで見ているので自然に「不明なコマンド」へ流れます。 辞書ディスパッチは名前しか見ていないため、引数の数は各ハンドラの中で確かめる必要があります。if-elif は同じ結果になりますが、len(parts) == 2 and parts[0] == "go" と parts[1] のように、長さの確認と取り出しを別々に書くことになります。

どれを選ぶかの判断表

やりたいこと 向いている書き方 理由
値が決まった数種類のどれかで分ける match(リテラル・OR)または if-elif どちらでも短く書ける
リストの長さや辞書のキーなど「形」で分ける match 形の確認と取り出しが1行で済む
型ごとに処理を変える match(クラスパターン) isinstance の連続より読みやすい
範囲や大小で分ける(80以上、60以上…) if-elif 比較が主役ならifのほうが素直
値と処理の対応を設定やプラグインで増やしたい 辞書ディスパッチ 実行時に登録・差し替えできる
値から値への単純な変換 辞書(d.get(key, 既定値)) 分岐を書かずに済む
Python 3.9以前でも動かす if-elif または辞書 match文は3.10以降

「値だけで分けるならどれでもよい。形や型を見ながら中身を取り出すならmatch」 と覚えておくと選びやすくなります。

JSON風データをmatch caseで分解する

APIの応答やイベントログのような入れ子のデータは、match文が特に読みやすくなる題材です。辞書・リスト・クラスパターン・ガード・asを組み合わせます。

# u2.py
import json

events = json.loads("""
[
  {"type": "order", "id": 101, "items": [{"name": "pen", "qty": 2}, {"name": "note", "qty": 1}]},
  {"type": "order", "id": 102, "items": []},
  {"type": "member", "action": "join", "user": {"name": "sato", "tags": ["new", "mail"]}},
  {"type": "member", "action": "leave", "user": {"name": "kato"}},
  {"type": "stock", "sku": "A-1", "delta": -3},
  {"type": "stock", "sku": "A-2", "delta": "many"},
  {"type": "unknown"},
  ["not", "a", "dict"]
]
""")

def handle(ev):
    match ev:
        case {"type": "order", "id": int(oid), "items": []}:
            return f"注文{oid}: 明細が空"
        case {"type": "order", "id": int(oid), "items": [{"name": str(first)}, *others]}:
            return f"注文{oid}: 先頭は{first}、ほか{len(others)}件"
        case {"type": "member", "action": "join" | "rejoin" as act,
              "user": {"name": name, "tags": [*tags]}}:
            return f"会員{act}: {name} タグ={tags}"
        case {"type": "member", "action": act, "user": {"name": name}}:
            return f"会員{act}: {name}"
        case {"type": "stock", "sku": sku, "delta": int(d)} if d < 0:
            return f"在庫減: {sku} {d}"
        case {"type": "stock", "sku": sku, "delta": d}:
            return f"在庫: {sku} の delta が想定外の値 {d!r}"
        case {"type": t}:
            return f"未対応の type: {t}"
        case _:
            return f"辞書ではない入力: {ev!r}"

for ev in events:
    print(handle(ev))

実行結果:

注文101: 先頭はpen、ほか1件
注文102: 明細が空
会員join: sato タグ=['new', 'mail']
会員leave: kato
在庫減: A-1 -3
在庫: A-2 の delta が想定外の値 'many'
未対応の type: unknown
辞書ではない入力: ['not', 'a', 'dict']

このコードで使っている書き方を整理します。

書き方 意味
"id": int(oid) 値が int のときだけマッチし、oid に取り出す(型の確認と取り出しを同時に行う)
"items": [] 空のリストのときだけマッチ(シーケンスの [] は長さ0限定)
[{"name": str(first)}, *others] 先頭の要素は name を持つ辞書、残りは others へ
"join" | "rejoin" as act どちらかに当てはまり、当てはまったほうを act に入れる
"user": {"name": name} 入れ子の辞書もそのまま書ける(ここも部分一致)
if d < 0 取り出した値に条件を付ける
case {"type": t} type はあるが未対応のものを拾う
case _ 辞書ですらない入力を拾う

並べる順番は「条件が細かいものを上、ゆるいものを下」 にします。辞書は部分一致なので、{"type": "order"} のようなゆるいパターンを上に置くと、下の細かいパターンに届かなくなります。この場合はSyntaxErrorにならず静かに動くため、順番は自分で確かめる必要があります。

dataclassと__match_args__でクラスを分解する

クラスパターンで Point(0, 5) のように位置で書けるかどうかは、クラスの __match_args__ で決まります。 これは「位置の何番目がどの属性か」を表すタプルです。dataclass は自動で用意してくれます。

# u1.py
from dataclasses import dataclass

@dataclass
class Order:
    id: int
    status: str
    items: list

class Member:
    __match_args__ = ("name", "rank")
    def __init__(self, name, rank):
        self.name = name
        self.rank = rank

class Plain:
    def __init__(self, name):
        self.name = name

print("Order.__match_args__ =", Order.__match_args__)

def describe(obj):
    match obj:
        case Order(_, "shipped", [first, *_]):
            return f"発送済み。最初の品: {first}"
        case Order(id=i, status="new"):
            return f"新規注文 {i}"
        case Member("admin", _):
            return "管理者"
        case Member(name, rank="gold"):
            return f"ゴールド会員 {name}"
        case Plain(name=n):
            return f"Plain {n}"
        case _:
            return "その他"

for o in [Order(1, "shipped", ["pen", "note"]), Order(2, "new", []),
          Member("admin", "x"), Member("sato", "gold"), Plain("p"),
          Order(3, "shipped", [])]:
    print(describe(o))

try:
    match Plain("p"):
        case Plain("p"):
            print("matched")
except TypeError as e:
    print("TypeError:", e)

try:
    match Order(1, "new", []):
        case Order(a, b, c, d):
            print("matched")
except TypeError as e:
    print("TypeError:", e)

実行結果:

Order.__match_args__ = ('id', 'status', 'items')
発送済み。最初の品: pen
新規注文 2
管理者
ゴールド会員 sato
Plain p
その他
TypeError: Plain() accepts 0 positional sub-patterns (1 given)
TypeError: Order() accepts 3 positional sub-patterns (4 given)
  • dataclass はフィールドの定義順に __match_args__ が作られます(('id', 'status', 'items'))。そのため Order(_, "shipped", ...) と位置で書けます。
  • 自作クラスは __match_args__ を自分で書くと位置で書けるようになります(Member)。
  • __match_args__ が無いクラスを位置で書くと TypeError です(Plain)。これは構文エラーではなく、そのcaseを試した時点で出る実行時のエラーです。キーワード形式(Plain(name=n))なら __match_args__ なしで使えます。
  • 位置の数が __match_args__ より多い場合も TypeError になります。
  • 最後の Order(3, "shipped", []) が「その他」になったのは、[first, *_] が空リストにマッチしないためです。
クラスの種類 位置で書ける キーワードで書ける
dataclass 書ける(定義順) 書ける
__match_args__ を定義したクラス 書ける 書ける
何も定義していない普通のクラス TypeError 書ける
int・str・list などの組み込み型 1つだけ書ける(値そのもの) 属性があれば書ける

なお、クラスパターンは内部で isinstance を使うので、サブクラスのインスタンスは親クラスのパターンにもマッチします。親と子の両方を書くときは、子クラスのcaseを上に置きます。

つまずきやすい点のチェックリスト

実際に動かして確かめた挙動を表にまとめます。

書いたもの 実際の動き(Python 3.11.15) こうするとよい
case 1: に True や 1.0 を渡す マッチする(True == 1、1.0 == 1) 区別したいときは case bool(): を先に、または case int(1) のように型も書く
case 0: に False や 0.0 を渡す マッチする 同上
case True: に 1 を渡す マッチしない(True・False・None は is で比較) 仕様どおり。真偽値だけを選べる
case int(): と case bool(): の順番 True は int にもマッチ(bool は int のサブクラス) case bool(): を上に書く
どのcaseにも当てはまらない 何も実行されない case _: で例外を投げるかログを出す
case int:(かっこなし) キャプチャになり何にでもマッチ case int(): と書く
case {}: どんな辞書にもマッチ 空だけなら case {**rest} if not rest:
文字列 "ab" を case [a, b] で受ける マッチしない list(s) にしてから渡す
ガードが偽で次へ進む キャプチャ済みの変数には値が入っている 変数名の使い回しに注意する

True と 1 の区別は、実行して確かめた結果が次のとおりです。

for v in [True, 1]:
    match v:
        case bool():
            r = "bool"
        case int():
            r = "int"
    print(f"{v!r} -> {r}")
True -> bool
1 -> int

出会いやすいエラー文の一覧

エラーの文面 原因 直し方
SyntaxError: invalid syntax(match x: の行) Python 3.9以前で実行している 3.10以上で実行する
SyntaxError: name capture 'X' makes remaining patterns unreachable 名前だけを書いてキャプチャになっている Enum名.X のようにドット付きで書く
SyntaxError: wildcard makes remaining patterns unreachable case _: の後ろにcaseがある case _: を最後に移す
SyntaxError: patterns may only match literals and attribute lookups f文字列や関数呼び出しをパターンに書いた ガードで比較する
SyntaxError: alternative patterns bind different names ORの選択肢ごとに取り出す名前が違う 全ての選択肢で同じ名前にする
SyntaxError: multiple assignments to name 'a' in pattern 同じ名前を1つのパターンで2回使った 別の名前にしてガードで比べる
SyntaxError: multiple starred names in sequence pattern * を2回書いた * は1つにする
TypeError: X() accepts 0 positional sub-patterns (1 given) __match_args__ の無いクラスを位置で書いた キーワード形式にするか __match_args__ を定義する

よくある質問

Q. match caseはswitch文と同じものですか? A. 値で分岐するという使い方は同じですが、できることはより広いです。リストの長さ、辞書のキー、クラスの属性といった「形」を調べながら、中の値を変数に取り出せます。また、次のcaseへ流れ落ちる動きは無いので、breakを書く必要はありません。複数の値をまとめたいときは case 1 | 2 | 3: とORパターンで書きます。

Q. デフォルト(どれにも当てはまらないとき)はどう書きますか? A. 最後に case _: を書きます。_ はワイルドカードで、何にでもマッチし、変数には値を入れません。case _: は必ず最後に置きます。途中に置くと「SyntaxError: wildcard makes remaining patterns unreachable」になります。

Q. match文はif-elifより速いですか? A. この記事では速度の計測は行っていません(未実行)。match文は上から順にパターンを試す仕組みなので、選ぶ基準は速度よりも読みやすさに置くのがおすすめです。形や型を見ながら中身を取り出す処理はmatch、大小の比較が中心ならif-elifが書きやすくなります。

Q. 正規表現や「〜で始まる文字列」でマッチできますか? A. パターンそのものには書けませんが、ガードと組み合わせれば実現できます。たとえば case str() as s if s.startswith("ERR"): のように書きます。正規表現も case str() as s if re.fullmatch(r"\d+", s): のようにガードの中で使えます。

まとめ

  • match case文はPython 3.10以降の構文で、上から順にパターンを試し、最初に当てはまったcaseだけを実行します。3.9以前では SyntaxError: invalid syntax になります。
  • パターンはリテラル・OR・シーケンス・辞書・クラス・ガード・as・キャプチャ・ワイルドカード・値の組み合わせで、形の確認と値の取り出しを同時に行えます。
  • ドットの付かない名前はキャプチャになり、何にでもマッチします。定数と比べたいときは Code.NOT_FOUND のようにドット付きの名前(Enumなど)で書きます。
  • 辞書パターンは部分一致、case {}: はどんな辞書にもマッチします。厳密にしたいときは **rest とガードを使います。
  • 文字列・bytes・bytearray はシーケンスパターンにマッチしません。 1文字ずつ分けたいときは list() にしてから渡します。
  • dataclass は __match_args__ が自動で作られるので位置で書けます。自作クラスは __match_args__ を定義するか、キーワード形式で書きます。
  • 値だけの分岐ならif-elifや辞書でも十分です。形や型を見ながら中身を取り出す処理で、match文の読みやすさが生きます。

参考・出典

  • Python 公式ドキュメント「言語リファレンス 8. 複合文 ― match 文」(3.14版。パターンの種類、値パターン、シーケンスパターンの対象外の型、__match_args__、何にでもマッチするcaseは最後に1つだけ、という記述を確認) https://docs.python.org/ja/3/reference/compound_stmts.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python match caseの使い方|パターン全種の早見表と注意点」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?