Skip to content
주제
Python
Python Switch Case: match-case 문 설명

Python Switch Case: match-case 문 설명

업데이트

Python에서 명령 처리기를 작성하고 있다고 해보겠습니다. 이 함수는 "quit", "save", "load", "help" 같은 문자열을 받아서 각 명령마다 서로 다른 로직을 실행해야 합니다. JavaScript나 C라면 switch 문을 사용하겠지만, Python에서는 if/elif/else 블록을 연달아 작성하게 됩니다. 명령이 두 개면 다섯 줄, 여섯 개면 열다섯 줄, 열두 개면 서른 줄이 됩니다. 함수는 읽기 어려운 비교문의 벽처럼 커지고, 순서를 잘못 배치하기 쉽고, 확장하기도 번거로워집니다.

검색어가 python switch case라면 여기서 시작하세요. 더 깊은 구조적 패턴 매칭 참고가 필요하다면 Python Match Case를 보세요. 더 단순한 dictionary-dispatch 개요는 Python Switch Case를 참고하세요.

이것은 30년 넘게 Python의 실제 제약이었습니다. 다른 주요 언어들은 모두 switch 또는 case 구문을 가지고 있었지만, Python은 전적으로 if/elif 체인과 dictionary dispatch에 의존했습니다. Guido van Rossum은 여러 차례 switch-case 제안을 거부하며 if/elif로 충분하다고 주장했습니다.

그 변화는 Python 3.10에서 PEP 634 (opens in a new tab)와 함께 찾아왔습니다. 이때 match-case 문이 도입되었고, Python의 switch-case에 해당하는 기능이 생겼습니다. 하지만 이것은 단순한 switch 대체가 아니라 훨씬 강력합니다. 값만 비교하는 것이 아니라 객체를 분해하고, 패턴을 매칭하고, 변수를 바인딩하고, 조건을 적용할 수 있습니다. 즉, 단순한 switch가 아니라 구조적 패턴 매칭입니다.

30초 만에 보는 빠른 문법: match-case

def handle_command(command):
    match command:
        case "quit":
            print("Exiting program")
        case "save":
            print("Saving file")
        case "help":
            print("Showing help")
        case _:
            print(f"Unknown command: {command}")
 
handle_command("save")   # Saving file
handle_command("hello")  # Unknown command: hello

match 키워드는 subject expression을 한 번만 평가합니다. 각 case는 패턴을 정의합니다. Python은 패턴을 위에서 아래로 검사하고, 처음으로 일치하는 경우를 실행합니다. 와일드카드 _는 기본 케이스로, 어떤 값이든 매칭됩니다.

match-case vs switch-case: 핵심 차이점

C, Java, JavaScript, Go에서 넘어왔다면 다음 차이점이 중요합니다.

FeaturePython match-caseC/Java switch-case
Fall-throughNo (each case is independent)Yes (requires break)
Pattern typesLiterals, sequences, mappings, classes, OR, guardsMostly literals only
DestructuringBuilt-in (bind variables from matched structure)Not supported
Variable bindingCaptures values from matched patternNot supported
Default casecase _:default:
Expression/statementStatement (no return value)Statement
Minimum Python3.10+N/A

가장 중요한 차이점은 fall-through가 없다는 것입니다. 각 case 블록은 독립적입니다. 따라서 break를 쓸 필요가 없고, 다음 case가 실수로 실행되는 일도 없습니다.

Pattern Types: match-case가 할 수 있는 일

Python의 match-case는 7가지 패턴 타입을 지원합니다. 이것이 전통적인 switch 문보다 근본적으로 더 강력한 이유입니다.

1. Literal Patterns

문자열, 숫자, 불리언, None 같은 정확한 값을 매칭합니다:

def classify_status(code):
    match code:
        case 200:
            return "OK"
        case 301:
            return "Moved Permanently"
        case 404:
            return "Not Found"
        case 500:
            return "Internal Server Error"
        case _:
            return f"Unknown status: {code}"
 
print(classify_status(404))  # Not Found

2. OR Patterns (Multiple Values)

| 연산자를 사용해 한 case에서 여러 값을 매칭할 수 있습니다:

def classify_http_status(code):
    match code:
        case 200 | 201 | 204:
            return "Success"
        case 301 | 302 | 307 | 308:
            return "Redirect"
        case 400 | 401 | 403 | 404:
            return "Client Error"
        case 500 | 502 | 503:
            return "Server Error"
        case _:
            return "Unknown"

이것은 흔한 if code in (200, 201, 204): 패턴을 대체합니다.

3. Capture Patterns (Variable Binding)

case 안의 bare name은 매칭된 값을 변수에 캡처합니다:

def process_value(data):
    match data:
        case 0:
            print("Zero")
        case value:
            print(f"Got non-zero value: {value}")
 
process_value(42)  # Got non-zero value: 42

중요한 함정: bare name은 항상 캡처합니다. 기존 변수와 비교하는 것이 아닙니다. 이것은 가장 흔한 실수입니다:

# WRONG — this does NOT compare against expected_code
expected_code = 200
match response_code:
    case expected_code:  # This captures response_code into expected_code!
        print("Match")
 
# RIGHT — use a guard or literal
match response_code:
    case code if code == expected_code:
        print("Match")

상수와 비교하려면 dotted name(예: http.HTTPStatus.OK)이나 guard clause를 사용하세요.

4. Sequence Patterns (Lists and Tuples)

리스트와 튜플을 패턴 안에서 직접 분해합니다:

def process_point(point):
    match point:
        case (0, 0):
            return "Origin"
        case (x, 0):
            return f"On x-axis at {x}"
        case (0, y):
            return f"On y-axis at {y}"
        case (x, y):
            return f"Point at ({x}, {y})"
 
print(process_point((3, 0)))   # On x-axis at 3
print(process_point((2, 5)))   # Point at (2, 5)

*rest를 사용해 나머지 요소를 캡처할 수 있습니다:

def first_and_rest(items):
    match items:
        case []:
            return "Empty list"
        case [single]:
            return f"Single item: {single}"
        case [first, *rest]:
            return f"First: {first}, remaining: {rest}"
 
print(first_and_rest([1, 2, 3, 4]))  # First: 1, remaining: [2, 3, 4]

5. Mapping Patterns (Dictionaries)

dictionary의 키를 매칭하고 값을 분해합니다:

def handle_event(event):
    match event:
        case {"type": "click", "x": x, "y": y}:
            print(f"Click at ({x}, {y})")
        case {"type": "keypress", "key": key}:
            print(f"Key pressed: {key}")
        case {"type": "scroll", "direction": direction}:
            print(f"Scroll {direction}")
        case _:
            print("Unknown event")
 
handle_event({"type": "click", "x": 100, "y": 200})
# Click at (100, 200)
 
handle_event({"type": "keypress", "key": "Enter", "modifiers": ["Ctrl"]})
# Key pressed: Enter  (extra keys are ignored)

mapping pattern은 지정한 키가 존재하면 매칭되며, dictionary의 추가 키는 조용히 무시됩니다. 따라서 JSON 데이터나 API 응답 처리에 적합합니다.

6. Class Patterns

클래스 인스턴스를 매칭하고 속성을 추출합니다:

from dataclasses import dataclass
 
@dataclass
class Point:
    x: float
    y: float
 
@dataclass
class Circle:
    center: Point
    radius: float
 
@dataclass
class Rectangle:
    corner: Point
    width: float
    height: float
 
def describe_shape(shape):
    match shape:
        case Circle(center=Point(x=0, y=0), radius=r):
            return f"Circle at origin with radius {r}"
        case Circle(center=center, radius=r):
            return f"Circle at ({center.x}, {center.y}) with radius {r}"
        case Rectangle(width=w, height=h) if w == h:
            return f"Square with side {w}"
        case Rectangle(width=w, height=h):
            return f"Rectangle {w}x{h}"
 
print(describe_shape(Circle(Point(0, 0), 5)))
# Circle at origin with radius 5
 
print(describe_shape(Rectangle(Point(1, 1), 4, 4)))
# Square with side 4

7. Guard Clauses

추가 필터링을 위해 어떤 패턴에도 if 조건을 붙일 수 있습니다:

def classify_number(n):
    match n:
        case n if n < 0:
            return "Negative"
        case 0:
            return "Zero"
        case n if n % 2 == 0:
            return "Positive even"
        case _:
            return "Positive odd"
 
print(classify_number(-5))  # Negative
print(classify_number(4))   # Positive even
print(classify_number(7))   # Positive odd

guard는 패턴만으로 표현할 수 없는 추가 조건을 붙일 수 있게 해줍니다. 먼저 패턴이 매칭되고, 그 다음 guard 조건이 평가됩니다.

실전 예시: JSON API 응답 파서

match-case의 가장 실용적인 활용처 중 하나는 API 응답이나 JSON 데이터를 파싱하는 것입니다:

import json
 
def process_api_response(response: dict) -> str:
    match response:
        case {"status": "success", "data": {"users": [*users]}}:
            return f"Found {len(users)} users"
 
        case {"status": "success", "data": {"user": {"name": name, "email": email}}}:
            return f"User: {name} ({email})"
 
        case {"status": "error", "code": 401}:
            return "Authentication required"
 
        case {"status": "error", "code": code, "message": msg}:
            return f"Error {code}: {msg}"
 
        case {"status": "error"}:
            return "Unknown error"
 
        case _:
            return "Unrecognized response format"
 
# Test with different responses
print(process_api_response({
    "status": "success",
    "data": {"users": ["Alice", "Bob", "Charlie"]}
}))
# Found 3 users
 
print(process_api_response({
    "status": "error",
    "code": 429,
    "message": "Rate limit exceeded"
}))
# Error 429: Rate limit exceeded

이것은 isinstance() 검사와 dictionary .get() 호출이 깊게 중첩된 if/elif 체인을 대체합니다. match-case 버전은 기대하는 구조를 직접 표현합니다.

실전 예시: 커맨드라인 인자 파서

import sys
 
def parse_args(args: list[str]):
    match args:
        case [program]:
            print(f"Usage: {program} <command> [options]")
 
        case [_, "init", project_name]:
            print(f"Initializing project: {project_name}")
 
        case [_, "init"]:
            print("Error: project name required")
 
        case [_, "build", "--release"]:
            print("Building in release mode")
 
        case [_, "build"]:
            print("Building in debug mode")
 
        case [_, "test", *test_files] if test_files:
            print(f"Running tests: {', '.join(test_files)}")
 
        case [_, "test"]:
            print("Running all tests")
 
        case [_, unknown, *_]:
            print(f"Unknown command: {unknown}")
 
parse_args(["app", "init", "my-project"])  # Initializing project: my-project
parse_args(["app", "test", "a.py", "b.py"])  # Running tests: a.py, b.py
parse_args(["app", "build", "--release"])  # Building in release mode

3.10 이전 대안: match-case 없이 Switch-Case 구현하기

Python 3.9 이하를 사용하고 있거나, 단순한 값 매칭에 match-case가 과하다고 느껴진다면 다음과 같은 대안이 널리 쓰입니다.

if/elif/else Chains

가장 직관적인 방법입니다:

def handle_command(command):
    if command == "quit":
        print("Exiting program")
    elif command == "save":
        print("Saving file")
    elif command == "load":
        print("Loading file")
    elif command == "help":
        print("Showing help")
    else:
        print(f"Unknown command: {command}")

사용 시점: 5~6개 미만의 case이고 로직이 단순할 때.

Dictionary Dispatch

dictionary를 사용해 값을 함수에 매핑합니다:

def cmd_quit():
    print("Exiting program")
 
def cmd_save():
    print("Saving file")
 
def cmd_help():
    print("Showing help")
 
commands = {
    "quit": cmd_quit,
    "save": cmd_save,
    "help": cmd_help,
}
 
def handle_command(command):
    action = commands.get(command)
    if action:
        action()
    else:
        print(f"Unknown command: {command}")

사용 시점: 값을 단순한 동작에 매핑할 때, 특히 case 수가 많을 때. if/elif의 O(n)보다 O(1) 조회가 가능합니다.

Dictionary Dispatch with Arguments

def calculate(operation, a, b):
    ops = {
        "+": lambda a, b: a + b,
        "-": lambda a, b: a - b,
        "*": lambda a, b: a * b,
        "/": lambda a, b: a / b if b != 0 else float("inf"),
    }
    func = ops.get(operation)
    if func is None:
        raise ValueError(f"Unknown operation: {operation}")
    return func(a, b)
 
print(calculate("+", 10, 3))  # 13
print(calculate("/", 10, 0))  # inf

Enum-Based Dispatch

타입이 명확하고 자기 문서화되는 코드를 원할 때:

from enum import Enum
 
class Direction(Enum):
    NORTH = "north"
    SOUTH = "south"
    EAST = "east"
    WEST = "west"
 
def move(direction: Direction, x: int, y: int) -> tuple[int, int]:
    offsets = {
        Direction.NORTH: (0, 1),
        Direction.SOUTH: (0, -1),
        Direction.EAST: (1, 0),
        Direction.WEST: (-1, 0),
    }
    dx, dy = offsets[direction]
    return (x + dx, y + dy)
 
print(move(Direction.NORTH, 0, 0))  # (0, 1)

비교: 어떤 방식을 써야 할까

ApproachPython VersionBest ForComplexityPerformance
match-case3.10+Complex patterns, destructuring, type matchingHigh expressivenessComparable to if/elif
if/elif/elseAllSimple value comparisons, < 6 casesSimpleO(n) linear scan
Dict dispatchAllMany cases, value → function mappingMediumO(1) lookup
Dict + lambdaAllMany cases, value → expression mappingMediumO(1) lookup
Enum dispatchAllTyped constants, exhaustive matchingMediumO(1) lookup

Rule of thumb:

  • Use match-case when you need destructuring, type matching, or complex patterns
  • Use if/elif for simple comparisons with few branches
  • Use dict dispatch for many simple value-to-action mappings
  • Use enum dispatch when the set of values is fixed and typed

자주 쓰는 패턴과 레시피

Type-Based Dispatch

def serialize(value):
    match value:
        case bool():  # Must come before int — bool is a subclass of int
            return "true" if value else "false"
        case int() | float():
            return str(value)
        case str():
            return f'"{value}"'
        case list():
            items = ", ".join(serialize(v) for v in value)
            return f"[{items}]"
        case dict():
            pairs = ", ".join(
                f'{serialize(k)}: {serialize(v)}' for k, v in value.items()
            )
            return "{" + pairs + "}"
        case None:
            return "null"
        case _:
            raise TypeError(f"Cannot serialize {type(value)}")
 
print(serialize({"name": "Alice", "scores": [95, 87, 92], "active": True}))
# {"name": "Alice", "scores": [95, 87, 92], "active": true}

중요: TrueFalse가 Python에서 int의 하위 클래스이므로 bool()int()보다 앞에 두어야 합니다.

Nested Pattern Matching

def evaluate(expr):
    """Simple math expression evaluator."""
    match expr:
        case int(n) | float(n):
            return n
        case ("+", left, right):
            return evaluate(left) + evaluate(right)
        case ("-", left, right):
            return evaluate(left) - evaluate(right)
        case ("*", left, right):
            return evaluate(left) * evaluate(right)
        case ("/", left, right):
            divisor = evaluate(right)
            if divisor == 0:
                raise ZeroDivisionError("Division by zero")
            return evaluate(left) / divisor
        case ("neg", operand):
            return -evaluate(operand)
        case _:
            raise ValueError(f"Invalid expression: {expr}")
 
# (3 + 4) * 2
result = evaluate(("*", ("+", 3, 4), 2))
print(result)  # 14

match-case로 상태 머신 만들기

def tokenize(text):
    """Simple tokenizer using match-case as a state machine."""
    tokens = []
    i = 0
    while i < len(text):
        match text[i]:
            case ' ' | '\t' | '\n':
                i += 1  # Skip whitespace
            case '+' | '-' | '*' | '/':
                tokens.append(("OP", text[i]))
                i += 1
            case '(' | ')':
                tokens.append(("PAREN", text[i]))
                i += 1
            case c if c.isdigit():
                j = i
                while j < len(text) and text[j].isdigit():
                    j += 1
                tokens.append(("NUM", int(text[i:j])))
                i = j
            case c if c.isalpha():
                j = i
                while j < len(text) and text[j].isalnum():
                    j += 1
                tokens.append(("ID", text[i:j]))
                i = j
            case c:
                raise SyntaxError(f"Unexpected character: {c}")
    return tokens
 
print(tokenize("x + 42 * (y - 3)"))
# [('ID', 'x'), ('OP', '+'), ('NUM', 42), ('OP', '*'), ('PAREN', '('),
#  ('ID', 'y'), ('PAREN', ')')]

성능: match-case vs if/elif vs dict

단순한 값 매칭에서는 세 방식 모두 충분히 빠르므로 실제로는 차이가 중요하지 않습니다. 문자열 10개를 매칭하는 벤치마크 결과는 다음과 같습니다:

import timeit
 
commands = ["cmd_0", "cmd_1", "cmd_2", "cmd_3", "cmd_4",
            "cmd_5", "cmd_6", "cmd_7", "cmd_8", "cmd_9"]
 
# if/elif approach
def if_elif_dispatch(cmd):
    if cmd == "cmd_0": return 0
    elif cmd == "cmd_1": return 1
    elif cmd == "cmd_2": return 2
    elif cmd == "cmd_3": return 3
    elif cmd == "cmd_4": return 4
    elif cmd == "cmd_5": return 5
    elif cmd == "cmd_6": return 6
    elif cmd == "cmd_7": return 7
    elif cmd == "cmd_8": return 8
    elif cmd == "cmd_9": return 9
    else: return -1
 
# dict dispatch approach
dispatch_dict = {f"cmd_{i}": i for i in range(10)}
def dict_dispatch(cmd):
    return dispatch_dict.get(cmd, -1)
 
# match-case approach
def match_dispatch(cmd):
    match cmd:
        case "cmd_0": return 0
        case "cmd_1": return 1
        case "cmd_2": return 2
        case "cmd_3": return 3
        case "cmd_4": return 4
        case "cmd_5": return 5
        case "cmd_6": return 6
        case "cmd_7": return 7
        case "cmd_8": return 8
        case "cmd_9": return 9
        case _: return -1
ApproachBest case (first match)Worst case (last match)Miss (no match)
if/elif~80ns~400ns~420ns
dict dispatch~60ns~60ns~65ns
match-case~90ns~450ns~470ns

핵심 요약: Dictionary dispatch는 위치와 무관하게 O(1)입니다. if/elif와 match-case는 모두 O(n) 선형 탐색입니다. case가 20개 미만이라면 차이는 거의 없습니다. 성능보다 가독성을 기준으로 선택하세요.

자주 하는 실수와 피하는 방법

Mistake 1: Python 3.10 이전에서 match-case 사용하기

# This causes SyntaxError in Python 3.9 and earlier
match command:  # SyntaxError: invalid syntax
    case "quit":
        pass

Python 버전을 확인하세요: python --version. 3.9 이하라면 if/elif 또는 dict dispatch를 사용하세요.

Mistake 2: 변수와 비교한다고 착각하기

# WRONG — captures, does NOT compare
STATUS_OK = 200
match response_code:
    case STATUS_OK:  # This always matches and overwrites STATUS_OK!
        print("OK")
 
# RIGHT — use dotted name
class Status:
    OK = 200
    NOT_FOUND = 404
 
match response_code:
    case Status.OK:
        print("OK")
    case Status.NOT_FOUND:
        print("Not Found")
 
# RIGHT — use a guard
match response_code:
    case code if code == STATUS_OK:
        print("OK")

Mistake 3: 와일드카드 기본 케이스를 빼먹기

# DANGEROUS — unmatched values silently do nothing
match command:
    case "save":
        save_file()
    case "load":
        load_file()
    # No default — "delete" silently falls through with no action
 
# SAFE — always include a default case
match command:
    case "save":
        save_file()
    case "load":
        load_file()
    case _:
        raise ValueError(f"Unknown command: {command}")

Mistake 4: 겹치는 패턴의 순서를 잘못 두기

# WRONG — the first case catches everything
match value:
    case x:            # Captures ANY value — always matches!
        print(f"Got: {x}")
    case 42:           # Never reached
        print("The answer")
 
# RIGHT — specific patterns first, general last
match value:
    case 42:
        print("The answer")
    case x:
        print(f"Got: {x}")

데이터 과학에서 match-case 사용하기

데이터 처리 파이프라인을 다룬다면, match-case는 서로 다른 데이터 형식과 정제 작업을 처리하는 데 유용합니다:

import csv
from pathlib import Path
 
def load_data(source):
    """Load data from different source types."""
    match source:
        case str() as path if path.endswith(".csv"):
            with open(path) as f:
                return list(csv.DictReader(f))
        case str() as path if path.endswith(".json"):
            import json
            with open(path) as f:
                return json.load(f)
        case list() as records:
            return records
        case dict() as single_record:
            return [single_record]
        case _:
            raise TypeError(f"Unsupported data source: {type(source)}")

대화형 데이터 탐색에는 PyGWalker (opens in a new tab) 같은 도구를 사용하면 plotting code를 직접 작성하지 않고도 DataFrame을 바로 시각화할 수 있습니다. 데이터 처리 결과를 빠르게 확인하고 싶을 때 유용합니다.

match-case를 사용해 복잡한 분기를 가진 데이터 처리 스크립트를 만들고 있다면, RunCell (opens in a new tab)은 실제 데이터 샘플로 패턴 매칭을 대화형으로 테스트할 수 있는 AI 기반 Jupyter 환경을 제공합니다.

dataclass와 named tuple에서의 match-case

Python의 match-case는 dataclass와 named tuple에서 특히 잘 동작합니다:

from dataclasses import dataclass
from typing import Optional
 
@dataclass
class LogEntry:
    level: str
    message: str
    error: Optional[Exception] = None
 
def handle_log(entry: LogEntry):
    match entry:
        case LogEntry(level="CRITICAL", error=err) if err is not None:
            send_alert(f"CRITICAL with error: {err}")
            restart_service()
        case LogEntry(level="CRITICAL", message=msg):
            send_alert(f"CRITICAL: {msg}")
        case LogEntry(level="ERROR", error=err) if err is not None:
            log_to_file(f"ERROR: {err}")
        case LogEntry(level="WARNING" | "ERROR", message=msg):
            log_to_file(msg)
        case LogEntry(level="INFO" | "DEBUG"):
            pass  # Ignore low-priority logs

자주 묻는 질문

Python에 switch 문이 있나요?

Python 3.10+에는 match-case 문이 있으며, 이것이 Python에서 switch-case에 가장 가까운 기능입니다. 구조적 패턴 매칭을 지원하므로 전통적인 switch보다 더 강력합니다. 객체를 분해하고, 패턴을 매칭하고, 변수를 바인딩할 수 있습니다. Python 3.9 이하에서는 if/elif/else 체인이나 dictionary dispatch를 대안으로 사용하세요.

match-case와 switch-case의 차이점은 무엇인가요?

Python의 match-case는 fall-through가 없고(break가 필요하지 않음), 패턴 매칭(시퀀스, 매핑, 클래스)을 지원하며, 매칭된 패턴에서 변수 추출과 바인딩이 가능합니다. 또한 if 조건을 이용한 guard clause도 지원합니다. C/Java의 전통적인 switch-case는 주로 literal value만 매칭합니다.

Python에서 match-case가 if/elif보다 빠른가요?

단순한 값 매칭에서는 둘의 성능이 비슷합니다. 둘 다 선형 탐색을 합니다. case가 많을 때는 dictionary dispatch가 더 빠를 수 있습니다(O(1)). case가 20개 미만이라면 성능 차이는 무시해도 좋습니다. 성능보다 가독성과 패턴의 복잡성을 기준으로 선택하세요.

Python 3.9에서 match-case를 사용할 수 있나요?

아니요. match-case 문은 Python 3.10 이상이 필요합니다. 그 이전 버전에서는 if/elif/else 체인이나 dictionary dispatch를 사용하세요.

왜 match-case에서 변수 비교가 작동하지 않나요?

case x: 같은 bare name은 기존 변수와 비교하지 않고, 어떤 값이든 x에 캡처합니다. 변수와 비교하려면 guard clause(case val if val == x:)를 사용하거나 dotted name(case MyClass.CONSTANT:)을 사용하세요.

match-case에서 underscore _는 무엇인가요?

_ 와일드카드 패턴은 어떤 값이든 매칭하지만 변수에 바인딩하지 않습니다. switch 문의 default:와 같은 기본 케이스입니다. 항상 마지막에 배치하세요.

Related Guides

📚