Python: уроки, справочник и задачи

Оператор match case в Python: структурное сопоставление с образцом

Раздел: Основы Python · Обновлено 25.10.2026
Программист пишет код на ноутбуке
Фото: неизвестен, CC0

Оператор match появился в Python 3.10 и часто воспринимается как долгожданный switch case. На самом деле он умеет намного больше: проверяет структуру данных, раскладывает списки и словари по переменным, сопоставляет объекты по атрибутам. Правильно примененный match заменяет десяток строк из isinstance, len и индексов.

Разберем все виды образцов на практических примерах и посмотрим, где match действительно лучше цепочки if/elif, а где нет. Базовые условные конструкции описаны в статье условия в Python.

Синтаксис и порядок проверки

Общая форма выглядит так:

match выражение:
    case образец_1:
        ...
    case образец_2 if условие:
        ...
    case _:
        ...  # все остальное

Python вычисляет выражение один раз и проверяет образцы сверху вниз. Выполняется только первый подошедший блок case, проваливания в следующий, как в C, нет, и break писать не нужно. Если ни один образец не подошел и нет case _, ничего не происходит, исключения не будет.

Слова match и case - мягкие ключевые слова. Это значит, что старый код с переменной match (например, результат re.match) продолжает работать.

Литералы: замена switch case

Самый простой случай - сравнение с константами: числами, строками, None, True, False.

def http_status(code: int) -> str:
    match code:
        case 200:
            return "OK"
        case 301 | 302:
            return "Перенаправление"
        case 404:
            return "Не найдено"
        case 500:
            return "Ошибка сервера"
        case _:
            return "Неизвестный код"

print(http_status(302))   # -> Перенаправление
print(http_status(418))   # -> Неизвестный код

Образец _ (wildcard) подходит к любому значению и играет роль ветки default. Его стоит ставить последним: образцы после него никогда не сработают, и интерпретатор сообщит об этом как об ошибке синтаксиса.

Литералы сравниваются через ==, а None, True и False - через is. Поэтому case True не сработает для числа 1, хотя 1 == True.

Несколько значений в одном case

Оператор | объединяет альтернативы: ветка срабатывает, если подходит любая из них. Так решается запрос "python match case несколько значений":

def day_type(day: str) -> str:
    match day.lower():
        case "сб" | "вс":
            return "выходной"
        case "пн" | "вт" | "ср" | "чт" | "пт":
            return "рабочий"
        case _:
            raise ValueError(f"неизвестный день: {day}")

print(day_type("Вс"))   # -> выходной

Захват значений и главная ловушка

Если в образце стоит простое имя, это не сравнение с переменной, а захват: образец подходит к любому значению и присваивает его этому имени.

command = "stop"
match command:
    case "start":
        print("запуск")
    case other:
        print(f"неизвестная команда: {other}")
# -> неизвестная команда: stop

Отсюда главная ловушка match. Попытка сравнить с константой, записанной в переменную, молча ломает логику:

OK = 200
NOT_FOUND = 404

status = 404
match status:
    case OK:            # это захват, а не сравнение с 200
        print("успех")
    case NOT_FOUND:
        print("не найдено")
# SyntaxError: name capture 'OK' makes remaining patterns unreachable

Здесь Python хотя бы выдаст ошибку, потому что после захватывающего образца другие ветки недостижимы. Но если такой case окажется последним, ошибки не будет, и он просто проглотит любое значение, перезаписав переменную. Чтобы сравнивать с константами, используйте имена с точкой: атрибуты модуля, класса или перечисления. Такие имена образец трактует как значение.

from enum import Enum
from http import HTTPStatus

class Color(Enum):
    RED = "red"
    GREEN = "green"

def describe(c: Color) -> str:
    match c:
        case Color.RED:
            return "стоп"
        case Color.GREEN:
            return "можно ехать"

status = 404
match status:
    case HTTPStatus.OK:
        print("успех")
    case HTTPStatus.NOT_FOUND:
        print("не найдено")
# -> не найдено

Перечисления Enum и match отлично дополняют друг друга. HTTPStatus наследует от int, поэтому сравнение с обычным числом работает.

Условия в образце: guard if

После образца можно добавить if с произвольным условием. Ветка выполнится, только если образец подошел и условие истинно. Захваченные переменные в условии уже доступны.

def classify(n: int) -> str:
    match n:
        case 0:
            return "ноль"
        case x if x < 0:
            return "отрицательное"
        case x if x % 2 == 0:
            return "четное положительное"
        case _:
            return "нечетное положительное"

print([classify(v) for v in (-3, 0, 4, 7)])
# -> ['отрицательное', 'ноль', 'четное положительное', 'нечетное положительное']

Если условие ложно, проверка продолжается со следующего case. Учтите, что захват в образце уже произошел, даже если guard не прошел: переменная x останется присвоенной.

Образцы последовательностей

Здесь match начинает по-настоящему выигрывать у if. Образец в квадратных или круглых скобках проверяет, что значение - последовательность нужной длины, и сразу раскладывает элементы по переменным.

def handle(command: str) -> str:
    match command.split():
        case []:
            return "пустая команда"
        case ["help"]:
            return "доступно: go, take, drop"
        case ["go", direction]:
            return f"идем на {direction}"
        case ["take", item]:
            return f"берем {item}"
        case ["take", *items]:
            return f"берем сразу: {', '.join(items)}"
        case ["drop", ("меч" | "щит") as item]:
            return f"бросаем {item}"
        case [verb, *_]:
            return f"не понимаю команду {verb}"

print(handle("go север"))            # -> идем на север
print(handle("take ключ лампа"))     # -> берем сразу: ключ, лампа
print(handle("drop щит"))            # -> бросаем щит
print(handle("drop камень"))         # -> не понимаю команду drop

Что тут происходит:

Важное исключение: строки, байты и bytearray не считаются последовательностями для match. Образец [a, b] не разберет строку "ab" на символы. Это сделано специально, иначе строки постоянно путались бы со списками.

Образцы словарей

Образец в фигурных скобках проверяет, что значение - отображение (например, dict), и в нем есть указанные ключи. Лишние ключи игнорируются, что идеально подходит для разбора JSON.

def process(event: dict) -> str:
    match event:
        case {"type": "click", "x": int(x), "y": int(y)}:
            return f"клик в точке ({x}, {y})"
        case {"type": "key", "key": str(key)} if len(key) == 1:
            return f"нажата клавиша {key}"
        case {"type": "resize", "size": [w, h]}:
            return f"новый размер {w}x{h}"
        case {"type": str(kind), **rest}:
            return f"событие {kind} без обработчика, поля: {sorted(rest)}"
        case _:
            return "некорректное событие"

print(process({"type": "click", "x": 10, "y": 20, "ts": 1700}))
# -> клик в точке (10, 20)
print(process({"type": "resize", "size": [800, 600]}))
# -> новый размер 800x600
print(process({"type": "scroll", "dy": -3}))
# -> событие scroll без обработчика, поля: ['dy']

Обратите внимание на int(x): это не вызов функции, а образец класса, который одновременно проверяет тип и захватывает значение. Если в JSON придет "x": "10", ветка не сработает. Конструкция **rest собирает остальные ключи в словарь. Если вы часто разбираете JSON-ответы, пригодится материал про модуль json.

Образцы классов

Образец вида ИмяКласса(...) проверяет isinstance и сопоставляет атрибуты объекта. Атрибуты можно указывать по именам:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

@dataclass
class Circle:
    center: Point
    r: float

@dataclass
class Rect:
    top_left: Point
    w: float
    h: float

def area(shape) -> float:
    match shape:
        case Circle(r=r):
            return 3.14159 * r ** 2
        case Rect(w=w, h=h) if w == h:
            return w * w
        case Rect(w=w, h=h):
            return w * h
        case _:
            raise TypeError(f"неизвестная фигура: {shape!r}")

def where(p: Point) -> str:
    match p:
        case Point(x=0, y=0):
            return "начало координат"
        case Point(x=0):
            return "на оси Y"
        case Point(0, y):              # позиционная форма
            return "недостижимо"
        case Point(x, 0):
            return f"на оси X, x={x}"
        case _:
            return "где-то на плоскости"

print(area(Rect(Point(0, 0), 3, 4)))   # -> 12
print(where(Point(5, 0)))              # -> на оси X, x=5

Позиционная форма Point(x, 0) работает благодаря атрибуту класса __match_args__, который задает порядок атрибутов. Dataclass создает его автоматически из порядка полей. Для обычного класса добавьте его вручную: __match_args__ = ("x", "y"). Без него позиционные образцы выбросят TypeError.

Ветка "недостижимо" в примере специально показывает порядок проверки: Point(0, y) никогда не сработает, потому что все точки с x == 0 уже перехватила ветка выше. Образцы классов удобно сочетать с иерархиями из статьи про классы и объекты.

Вложенные образцы: вычисление выражений

Образцы можно вкладывать друг в друга на любую глубину, и здесь match раскрывается полностью. Классический пример - вычисление арифметического выражения, записанного вложенными кортежами. Такой формат часто получается после разбора текста парсером.

def evaluate(expr):
    match expr:
        case int() | float() as number:
            return number
        case ("+", left, right):
            return evaluate(left) + evaluate(right)
        case ("*", left, right):
            return evaluate(left) * evaluate(right)
        case ("-", operand):
            return -evaluate(operand)
        case ("/", _, 0):
            raise ZeroDivisionError("деление на ноль в выражении")
        case ("/", left, right):
            return evaluate(left) / evaluate(right)
        case _:
            raise ValueError(f"неизвестное выражение: {expr!r}")

# (2 + 3) * -4
print(evaluate(("*", ("+", 2, 3), ("-", 4))))   # -> -20
print(evaluate(("/", 10, 4)))                   # -> 2.5

Каждая ветка одновременно проверяет, что выражение - кортеж нужной длины, что первый элемент - определенный оператор, и раскладывает операнды по переменным. Ветка с делением на ноль показывает, как литерал 0 (он равен и 0.0, ведь сравнение идет через ==) внутри последовательности отсекает частный случай раньше общего. Рекурсивный вызов evaluate разбирает вложенные выражения любой глубины. Тот же подход используют при обходе абстрактных синтаксических деревьев модуля ast: образцы классов вроде ast.BinOp(op=ast.Add()) читаются почти как описание грамматики.

Обратите внимание на int() | float() as number: пустые скобки в образце класса означают "только проверка типа, без разбора атрибутов". Заметьте также, что bool - подкласс int, поэтому True пройдет эту проверку. Если это нежелательно, поставьте выше отдельную ветку case bool(): с ошибкой.

Практика: обработчик сообщений чат-бота

Типичная задача, где match экономит много кода, - разбор входящих сообщений с разной структурой. Допустим, бот получает словари из API и должен реагировать на команды, текст и вложения:

def reply(message: dict) -> str:
    match message:
        case {"text": str(text)} if text.startswith("/"):
            match text.split():
                case ["/start"]:
                    return "Привет! Напишите /help"
                case ["/remind", minutes, *words] if minutes.isdigit() and words:
                    return f"Напомню через {minutes} мин: {' '.join(words)}"
                case [command, *_]:
                    return f"Команда {command} не поддерживается"
        case {"text": str(text)}:
            return f"Вы написали {len(text)} символов"
        case {"photo": [*sizes]} if sizes:
            return f"Фото получено, вариантов размера: {len(sizes)}"
        case {"document": {"file_name": str(name)}}:
            return f"Документ {name} сохранен"
        case _:
            return "Не понимаю такое сообщение"

print(reply({"text": "/remind 15 позвонить маме"}))
# -> Напомню через 15 мин: позвонить маме
print(reply({"document": {"file_name": "отчет.pdf", "size": 2048}}))
# -> Документ отчет.pdf сохранен
print(reply({"sticker": "cat"}))
# -> Не понимаю такое сообщение

Здесь показаны сразу несколько приемов. Внешний match определяет тип сообщения по набору ключей, вложенный разбирает текст команды на слова. Guard if minutes.isdigit() and words отсекает некорректные напоминания, которые затем попадают в общую ветку [command, *_]. Образец {"document": {"file_name": str(name)}} заглядывает во вложенный словарь, игнорируя лишние поля вроде размера. На цепочке if та же логика заняла бы втрое больше строк с ручными проверками isinstance и in.

Порядок веток снова важен: команды проверяются раньше обычного текста, потому что команда тоже подходит под образец {"text": str(text)}.

Проверка полноты с assert_never

Python не проверяет, что match обработал все возможные варианты: если в перечисление добавят новое значение, функция молча вернет None. Эту проблему решает статический анализатор типов (mypy, pyright) вместе с typing.assert_never, который появился в Python 3.11:

from enum import Enum
from typing import assert_never

class Status(Enum):
    NEW = "new"
    PAID = "paid"
    SHIPPED = "shipped"

def label(status: Status) -> str:
    match status:
        case Status.NEW:
            return "новый"
        case Status.PAID:
            return "оплачен"
        case Status.SHIPPED:
            return "отправлен"
        case _:
            assert_never(status)

print(label(Status.PAID))   # -> оплачен

Пока все значения перечисления обработаны, анализатор считает ветку case _ недостижимой, и тип status в ней сужается до Never. Стоит добавить Status.CANCELLED и забыть про него в match, как mypy сообщит об ошибке еще до запуска программы. Во время выполнения assert_never выбросит AssertionError, поэтому ошибка не пройдет незамеченной и без анализатора. Тот же прием работает с объединениями типов вроде int | str | None.

Типичные ошибки при использовании match

Виды образцов: сводная таблица

ОбразецПримерЧто проверяет
литералcase 404:равенство значению
захватcase x:подходит всегда, присваивает имя
wildcardcase _:подходит всегда, ничего не присваивает
значение с точкойcase Color.RED:равенство константе
альтернативаcase "a" | "b":любой из вариантов
последовательностьcase [x, *rest]:тип, длина, элементы
отображениеcase {"id": id_}:наличие ключей и их значения
классcase Point(x=0):isinstance и атрибуты
ascase [1, _] as pair:захват всего подобразца

match case или if/elif

Для простого сравнения одной переменной с тремя константами match не дает выигрыша: if/elif или словарь соответствий здесь не хуже. Словарь вообще часто оказывается лучшим "switch" в Python:

handlers = {"start": start, "stop": stop, "pause": pause}
handlers.get(command, unknown)()

А вот когда нужно одновременно проверить тип, длину, наличие ключей и достать вложенные значения, match выигрывает с большим отрывом. Сравните разбор события через if:

if (isinstance(event, dict) and event.get("type") == "resize"
        and isinstance(event.get("size"), (list, tuple))
        and len(event["size"]) == 2):
    w, h = event["size"]
    ...

и через match: case {"type": "resize", "size": [w, h]}:. Одна строка описывает форму данных, и ее читают как схему.

Короткое правило выбора:

Частые вопросы

Как сделать default в match case?

Последней веткой поставьте case _:. Она подходит к любому значению. Если нужно еще и использовать это значение, напишите case other:, и оно окажется в переменной other.

С какой версии Python работает match case?

С Python 3.10. На 3.9 и более старых версиях код с match выдаст SyntaxError еще на этапе разбора файла, поэтому библиотекам с поддержкой старых версий его использовать нельзя.

Почему case с моей переменной срабатывает всегда?

Простое имя в образце означает захват, а не сравнение. Используйте имя с точкой (Config.LIMIT, Color.RED) или guard: case x if x == limit:.

Можно ли разобрать строку по символам образцом последовательности?

Нет, строки специально исключены из образцов последовательностей. Разбейте строку заранее: match list(text): или match text.split():.

Можно ли в case проверять строку регулярным выражением?

Прямой поддержки нет: образцы сравнивают строки только на точное равенство. Есть два рабочих варианта. Первый - guard: case str(s) if re.fullmatch(r"\d{6}", s):. Второй - сначала выполнить re.match, а затем сопоставлять результат: match m.groups() if m else None: с образцами последовательностей для групп. Второй вариант удобен, когда регулярное выражение одно, а дальнейшая логика зависит от содержимого групп. Если шаблонов много и они разные, обычная цепочка if с re.fullmatch часто читается проще, чем десяток guard-условий.

Быстрее ли match, чем if/elif?

Нет, заметного выигрыша в скорости нет: образцы проверяются по очереди, как ветки if. Выбирайте match ради читаемости при разборе структур данных, а не ради производительности.