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: hellomatch 키워드는 subject expression을 한 번만 평가합니다. 각 case는 패턴을 정의합니다. Python은 패턴을 위에서 아래로 검사하고, 처음으로 일치하는 경우를 실행합니다. 와일드카드 _는 기본 케이스로, 어떤 값이든 매칭됩니다.
match-case vs switch-case: 핵심 차이점
C, Java, JavaScript, Go에서 넘어왔다면 다음 차이점이 중요합니다.
| Feature | Python match-case | C/Java switch-case |
|---|---|---|
| Fall-through | No (each case is independent) | Yes (requires break) |
| Pattern types | Literals, sequences, mappings, classes, OR, guards | Mostly literals only |
| Destructuring | Built-in (bind variables from matched structure) | Not supported |
| Variable binding | Captures values from matched pattern | Not supported |
| Default case | case _: | default: |
| Expression/statement | Statement (no return value) | Statement |
| Minimum Python | 3.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 Found2. 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 47. 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 oddguard는 패턴만으로 표현할 수 없는 추가 조건을 붙일 수 있게 해줍니다. 먼저 패턴이 매칭되고, 그 다음 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 mode3.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)) # infEnum-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)비교: 어떤 방식을 써야 할까
| Approach | Python Version | Best For | Complexity | Performance |
|---|---|---|---|---|
match-case | 3.10+ | Complex patterns, destructuring, type matching | High expressiveness | Comparable to if/elif |
if/elif/else | All | Simple value comparisons, < 6 cases | Simple | O(n) linear scan |
| Dict dispatch | All | Many cases, value → function mapping | Medium | O(1) lookup |
| Dict + lambda | All | Many cases, value → expression mapping | Medium | O(1) lookup |
| Enum dispatch | All | Typed constants, exhaustive matching | Medium | O(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}중요: True와 False가 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) # 14match-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| Approach | Best 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":
passPython 버전을 확인하세요: 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
- Python Try Except: Error Handling Guide
- Python Type Hints: Complete Tutorial
- Python Decorators Explained
- Python Collections Module Guide
- Python Enumerate Function
- Python Assert Statement