Python Match Case: 구조적 패턴 매칭 완전 해설 (Python 3.10+)
Updated on
Python 개발자들은 수십 년 동안 분기 로직을 처리하기 위해 if-elif 체인에 의존해 왔습니다. 변수를 여러 값과 비교하거나, 데이터의 구조를 검사하거나, 객체 타입에 따라 라우팅하는 작업은 길고 장황한 조건문 블록을 요구했고, 경우의 수가 늘어날수록 읽기 어려워졌습니다. 예를 들어 10가지 가능한 커맨드 타입에 따라 동작을 분기하는 함수는 elif 문으로 가득 찬 “벽”이 되어, 실제 로직이 반복되는 비교 코드 아래에 묻혀 버리기 쉽습니다.
이건 단순히 가독성 문제만이 아닙니다. 깊은 if-elif 체인은 미묘한 버그에 취약합니다. 조건 하나를 잘못 두거나, 어떤 케이스를 빠뜨리거나, 의도치 않게 잘못된 데이터가 다음 단계로 흘러가도 조용히 지나갈 수 있습니다. 다른 언어들은 오래전부터 패턴 매칭 구문으로 이런 문제를 해결해 왔지만, Python은 3.10 버전 전까지는 네이티브 해법이 없었습니다.
Python 3.10은 match-case 문(PEP 634)을 도입하며 언어에 구조적 패턴 매칭을 가져왔습니다. 이는 단순한 값 비교를 넘어서 시퀀스 분해, 딕셔너리 형태 매칭, 변수 바인딩, 가드 조건 적용, 클래스 타입 기반 디스패치까지 모두 깔끔하고 선언적인 문법으로 표현할 수 있습니다. 이 가이드는 모든 패턴 타입을 실용적인 예제와 함께 다룹니다.
구조적 패턴 매칭이란?
구조적 패턴 매칭은 값(“subject”)을 여러 패턴과 비교하고, 처음으로 매칭되는 패턴의 코드 블록을 실행하게 해줍니다. C나 Java의 switch 문과 달리, Python의 match-case는 값만 비교하지 않고 데이터의 구조를 검사합니다.
기본 문법:
match subject:
case pattern1:
# code for pattern1
case pattern2:
# code for pattern2
case _:
# default case (wildcard)핵심 특징:
- 패턴은 위에서 아래 순서로 평가되며, 처음 매칭되는 패턴이 승리합니다.
- 와일드카드 패턴
_는 무엇이든 매칭하며 기본(default) 케이스 역할을 합니다. - 패턴은 변수를 바인딩할 수 있습니다. 즉, 매칭된 값의 일부를 이름에 캡처하여 case 블록에서 사용할 수 있습니다.
- fall-through 동작이 없습니다. 오직 하나의 case 블록만 실행됩니다.
Python 버전 요구사항: match-case는 Python 3.10 이상에서만 사용할 수 있습니다. python --version으로 버전을 확인하세요. 더 오래된 버전을 사용 중이라면 이 기능을 사용하기 전에 업그레이드가 필요합니다.
리터럴 패턴: 정확한 값 매칭
match-case의 가장 단순한 사용은 리터럴 상수와 값을 비교하는 것입니다:
def get_http_status_message(code):
match code:
case 200:
return "OK"
case 201:
return "Created"
case 301:
return "Moved Permanently"
case 400:
return "Bad Request"
case 403:
return "Forbidden"
case 404:
return "Not Found"
case 500:
return "Internal Server Error"
case _:
return f"Unknown status code: {code}"
print(get_http_status_message(404)) # Not Found
print(get_http_status_message(418)) # Unknown status code: 418리터럴 패턴은 정수, 문자열, 불리언, None과 함께 동작합니다:
def describe_value(value):
match value:
case True:
return "Boolean true"
case False:
return "Boolean false"
case None:
return "None value"
case "":
return "Empty string"
case 0:
return "Zero"
case _:
return f"Other: {value}"중요: True, False, None은 동등성(==)이 아니라 동일성(즉 is)으로 매칭됩니다. 따라서 Python에서 1 == True는 True이지만, match 1은 case True에 매칭되지 않습니다.
Or-패턴: 여러 값 중 하나 매칭
파이프 연산자 |를 사용하면 하나의 case에서 여러 패턴 중 아무거나 매칭할 수 있습니다:
def classify_day(day):
match day.lower():
case "monday" | "tuesday" | "wednesday" | "thursday" | "friday":
return "Weekday"
case "saturday" | "sunday":
return "Weekend"
case _:
return "Invalid day"
print(classify_day("Saturday")) # Weekend
print(classify_day("Wednesday")) # Weekdayor-패턴은 리터럴뿐 아니라 모든 패턴 타입과 함께 사용할 수 있습니다:
def categorize_error(code):
match code:
case 400 | 401 | 403 | 404 | 405:
return "Client error"
case 500 | 502 | 503 | 504:
return "Server error"
case _:
return "Other"변수 캡처 패턴
패턴은 매칭된 값의 일부를 변수로 캡처할 수 있습니다. 패턴에서 “그냥 이름(bare name)”은 해당 위치에 오는 어떤 값이든 바인딩하는 변수로 동작합니다:
def parse_command(command):
match command.split():
case ["quit"]:
return "Exiting program"
case ["hello", name]:
return f"Hello, {name}!"
case ["add", x, y]:
return f"Sum: {int(x) + int(y)}"
case _:
return "Unknown command"
print(parse_command("hello Alice")) # Hello, Alice!
print(parse_command("add 3 5")) # Sum: 8
print(parse_command("quit")) # Exiting programcase ["hello", name]에서 변수 name은 리스트의 두 번째 위치에 있는 문자열이 무엇이든 캡처합니다. 이는 리터럴 매칭과 본질적으로 다릅니다. 비교 대상인 name 변수가 “이미 존재”하는 것이 아니라, 패턴이 새로운 바인딩을 생성합니다.
캡처 vs 상수 매칭 함정
bare name은 캡처 패턴이므로, 변수에 들어있는 값과 “비교”하는 방식으로는 바로 쓸 수 없습니다:
QUIT_COMMAND = "quit"
match user_input:
case QUIT_COMMAND: # This captures into QUIT_COMMAND, not compare!
print("This always matches!")상수와 비교하려면 점 표기(dotted name) 또는 리터럴 값을 사용해야 합니다:
class Commands:
QUIT = "quit"
HELP = "help"
match user_input:
case Commands.QUIT: # Dotted name: compares against the value
print("Quitting")
case Commands.HELP:
print("Showing help").이 포함된 이름(점 표기)은 캡처 패턴이 아니라 값 조회(value lookup)로 취급됩니다. 이는 PEP 634의 의도된 설계입니다.
시퀀스 패턴: 리스트와 튜플 분해
match-case는 시퀀스 분해에 특히 강력합니다. 리스트나 튜플의 길이와 내용을 함께 매칭할 수 있습니다:
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})"
case _:
return "Not a valid point"
print(process_point((0, 0))) # Origin
print(process_point((5, 0))) # On x-axis at 5
print(process_point((3, 7))) # Point at (3, 7)가변 길이 시퀀스를 위한 스타 패턴
*를 사용하면 가변 길이 시퀀스를 매칭할 수 있습니다:
def analyze_sequence(seq):
match seq:
case []:
return "Empty sequence"
case [single]:
return f"Single element: {single}"
case [first, second]:
return f"Pair: {first}, {second}"
case [first, *middle, last]:
return f"First: {first}, Last: {last}, Middle has {len(middle)} items"
print(analyze_sequence([])) # Empty sequence
print(analyze_sequence([42])) # Single element: 42
print(analyze_sequence([1, 2, 3, 4, 5])) # First: 1, Last: 5, Middle has 3 items중첩 시퀀스 패턴
패턴은 중첩하여 복잡한 데이터 구조도 매칭할 수 있습니다:
def process_matrix_row(row):
match row:
case [[a, b], [c, d]]:
return f"2x2 block: {a}, {b}, {c}, {d}"
case [first_row, *rest]:
return f"First row: {first_row}, remaining rows: {len(rest)}"
print(process_matrix_row([[1, 2], [3, 4]]))
# 2x2 block: 1, 2, 3, 4매핑 패턴: 딕셔너리 매칭
매핑 패턴은 딕셔너리(유사) 객체를 특정 키 존재 여부로 확인하고, 필요하면 값을 캡처합니다:
def handle_api_response(response):
match response:
case {"status": "success", "data": data}:
return f"Success! Data: {data}"
case {"status": "error", "message": msg}:
return f"Error: {msg}"
case {"status": "error", "code": code, "message": msg}:
return f"Error {code}: {msg}"
case {"status": status}:
return f"Unknown status: {status}"
case _:
return "Invalid response format"
print(handle_api_response({"status": "success", "data": [1, 2, 3]}))
# Success! Data: [1, 2, 3]
print(handle_api_response({"status": "error", "message": "Not found"}))
# Error: Not found매핑 패턴은 딕셔너리에 지정한 키가 포함되어 있으면 매칭되며, 추가 키는 무시됩니다. 이 특성은 JSON 데이터나 API 응답을 다룰 때 특히 유용합니다.
**rest로 나머지 키들을 캡처할 수 있습니다:
def extract_config(config):
match config:
case {"host": host, "port": port, **rest}:
return f"Server: {host}:{port}, extra config: {rest}"
case _:
return "Missing required config"
print(extract_config({"host": "localhost", "port": 8080, "debug": True}))
# Server: localhost:8080, extra config: {'debug': True}가드 절: 패턴에 조건 추가하기
가드는 패턴이 매칭된 뒤 추가로 참이어야 하는 if 조건을 붙입니다. 먼저 패턴을 확인하고, 매칭되면 가드 표현식을 평가합니다:
def classify_number(n):
match n:
case x if x < 0:
return "Negative"
case 0:
return "Zero"
case x if x % 2 == 0:
return "Positive even"
case x if x % 2 == 1:
return "Positive odd"
print(classify_number(-5)) # Negative
print(classify_number(0)) # Zero
print(classify_number(4)) # Positive even
print(classify_number(7)) # Positive odd패턴만으로 조건을 완전히 표현하기 어려울 때 가드는 필수입니다:
def validate_age(data):
match data:
case {"name": name, "age": age} if age < 0:
return f"Invalid: {name} has negative age"
case {"name": name, "age": age} if age < 18:
return f"{name} is a minor (age {age})"
case {"name": name, "age": age} if age >= 18:
return f"{name} is an adult (age {age})"
case _:
return "Invalid data format"
print(validate_age({"name": "Alice", "age": 25}))
# Alice is an adult (age 25)클래스 패턴: 객체 타입 매칭
클래스 패턴은 값이 특정 클래스의 인스턴스인지 확인하고, 선택적으로 속성을 분해합니다:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@dataclass
class Circle:
center: Point
radius: float
@dataclass
class Rectangle:
top_left: 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) if r > 100:
return f"Large circle at ({center.x}, {center.y})"
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(top_left=tl, width=w, height=h):
return f"Rectangle at ({tl.x}, {tl.y}), {w}x{h}"
case _:
return "Unknown shape"
print(describe_shape(Circle(Point(0, 0), 5)))
# Circle at origin with radius 5
print(describe_shape(Rectangle(Point(1, 2), 10, 10)))
# Square with side 10__match_args__가 없는 클래스는 키워드 인자를 사용해야 합니다. dataclass와 named tuple은 자동으로 __match_args__를 설정하므로 위치 기반 패턴도 사용할 수 있습니다:
@dataclass
class Color:
r: int
g: int
b: int
def describe_color(color):
match color:
case Color(0, 0, 0):
return "Black"
case Color(255, 255, 255):
return "White"
case Color(r, 0, 0):
return f"Red shade (r={r})"
case Color(0, g, 0):
return f"Green shade (g={g})"
case Color(0, 0, b):
return f"Blue shade (b={b})"
case Color(r, g, b):
return f"RGB({r}, {g}, {b})"
print(describe_color(Color(255, 0, 0))) # Red shade (r=255)
print(describe_color(Color(128, 64, 32))) # RGB(128, 64, 32)내장 타입 매칭
내장 타입을 이용해 타입 체크처럼 매칭할 수도 있습니다:
def process_value(value):
match value:
case int(n):
return f"Integer: {n}"
case float(n):
return f"Float: {n}"
case str(s):
return f"String: '{s}'"
case list(items):
return f"List with {len(items)} items"
case dict(d):
return f"Dict with keys: {list(d.keys())}"
case _:
return f"Other type: {type(value).__name__}"
print(process_value(42)) # Integer: 42
print(process_value("hello")) # String: 'hello'
print(process_value([1, 2, 3])) # List with 3 itemsmatch-case vs if-elif: 언제 무엇을 써야 할까
| 기준 | match-case | if-elif |
|---|---|---|
| Python 버전 | 3.10+ 전용 | 모든 버전 |
| 값 비교 | 깔끔(케이스마다 값 하나) | 같은 변수를 반복해 비교하므로 장황 |
| 구조 분해 | 내장 지원(시퀀스, dict, 객체) | 수동 언패킹 필요 |
| 타입 체크 | 네이티브 클래스 패턴 | isinstance() 호출 필요 |
| 가드 조건 | 패턴 뒤의 if | 일반 if/elif |
| 변수 바인딩 | 자동 캡처 | 수동 할당 |
| 성능 | 패턴 디스패치 최적화 | 순차 평가 |
| 가독성 | 구조가 있는 4+ 케이스에 유리 | 1~3개의 단순 조건에 유리 |
| fall-through | 없음(설계상) | elif 누락 시 가능 |
match-case가 확실히 더 좋은 경우:
# Parsing structured data -- match-case
def handle_event(event):
match event:
case {"type": "click", "x": x, "y": y}:
process_click(x, y)
case {"type": "keypress", "key": key, "modifiers": [*mods]}:
process_key(key, mods)
case {"type": "scroll", "delta": delta} if delta != 0:
process_scroll(delta)이를 if-elif로 쓰면:
# Same logic with if-elif -- more verbose
def handle_event(event):
if event.get("type") == "click" and "x" in event and "y" in event:
process_click(event["x"], event["y"])
elif event.get("type") == "keypress" and "key" in event and "modifiers" in event:
process_key(event["key"], event["modifiers"])
elif event.get("type") == "scroll" and event.get("delta", 0) != 0:
process_scroll(event["delta"])if-elif가 여전히 더 좋은 경우:
# Simple range checks -- if-elif is clearer
if temperature > 100:
status = "boiling"
elif temperature > 50:
status = "hot"
elif temperature > 20:
status = "warm"
else:
status = "cold"match-case는 패턴 자체로 “범위 비교”를 직접 지원하지 않습니다. 모든 케이스에 가드를 붙여야 하며, 그러면 가독성 이점이 사라집니다.
실무 활용 사례
커맨드라인 인자 파싱
import sys
def main():
match sys.argv[1:]:
case ["help"]:
print("Available commands: help, version, run, test")
case ["version"]:
print("v1.0.0")
case ["run", filename]:
print(f"Running {filename}")
case ["run", filename, "--verbose"]:
print(f"Running {filename} with verbose output")
case ["test", *test_files] if test_files:
print(f"Testing: {', '.join(test_files)}")
case ["test"]:
print("Running all tests")
case [unknown, *_]:
print(f"Unknown command: {unknown}")
case []:
print("No command provided. Use 'help' for usage.")상태 머신 구현
from dataclasses import dataclass
from typing import Optional
@dataclass
class State:
name: str
data: Optional[dict] = None
def transition(state, event):
match (state.name, event):
case ("idle", "start"):
return State("loading", {"progress": 0})
case ("loading", "progress"):
progress = state.data.get("progress", 0) + 25
if progress >= 100:
return State("ready", {"progress": 100})
return State("loading", {"progress": progress})
case ("loading", "cancel"):
return State("idle")
case ("ready", "process"):
return State("processing", state.data)
case ("processing", "complete"):
return State("done", {"result": "success"})
case ("processing", "error"):
return State("error", {"message": "Processing failed"})
case (_, "reset"):
return State("idle")
case (current, unknown_event):
print(f"No transition from '{current}' on '{unknown_event}'")
return state
# Usage
state = State("idle")
for event in ["start", "progress", "progress", "progress", "progress", "process", "complete"]:
state = transition(state, event)
print(f"Event: {event} -> State: {state.name}, Data: {state.data}")JSON API 응답 핸들러
def process_api_result(response):
match response:
case {"data": {"users": [*users]}, "meta": {"total": total}}:
print(f"Found {total} users, received {len(users)}")
for user in users:
match user:
case {"name": name, "role": "admin"}:
print(f" Admin: {name}")
case {"name": name, "role": role}:
print(f" {role.title()}: {name}")
case {"data": {"users": []}}:
print("No users found")
case {"error": {"code": code, "message": msg}} if code >= 500:
print(f"Server error ({code}): {msg}")
raise RuntimeError(msg)
case {"error": {"code": code, "message": msg}}:
print(f"Client error ({code}): {msg}")
case _:
print(f"Unexpected response format: {type(response)}")
# Example
process_api_result({
"data": {"users": [
{"name": "Alice", "role": "admin"},
{"name": "Bob", "role": "editor"}
]},
"meta": {"total": 2}
})설정 파일 파서
def apply_config(settings):
for key, value in settings.items():
match (key, value):
case ("database", {"host": host, "port": int(port), "name": db}):
print(f"DB connection: {host}:{port}/{db}")
case ("database", _):
raise ValueError("Invalid database config: need host, port, name")
case ("logging", {"level": level}) if level in ("DEBUG", "INFO", "WARNING", "ERROR"):
print(f"Log level set to {level}")
case ("logging", {"level": level}):
raise ValueError(f"Invalid log level: {level}")
case ("features", {"enabled": list(features)}):
print(f"Enabled features: {', '.join(features)}")
case ("features", {"enabled": _}):
raise TypeError("Features must be a list")
case (key, _):
print(f"Unknown config key: {key}")
apply_config({
"database": {"host": "localhost", "port": 5432, "name": "mydb"},
"logging": {"level": "INFO"},
"features": {"enabled": ["auth", "cache", "metrics"]}
})데이터 변환 파이프라인
def transform_record(record):
"""Transform raw data records into standardized format."""
match record:
case {"timestamp": ts, "value": float(v) | int(v), "unit": str(unit)}:
return {"time": ts, "measurement": float(v), "unit": unit.lower()}
case {"timestamp": ts, "value": str(v), "unit": str(unit)}:
try:
return {"time": ts, "measurement": float(v), "unit": unit.lower()}
except ValueError:
return {"time": ts, "measurement": None, "unit": unit.lower(), "error": f"Cannot parse: {v}"}
case {"timestamp": ts, "value": v}:
return {"time": ts, "measurement": float(v), "unit": "unknown"}
case {"values": [*values]} if values:
return [transform_record({"timestamp": None, "value": v, "unit": "batch"}) for v in values]
case _:
return {"error": f"Unrecognized format: {record}"}
# Examples
print(transform_record({"timestamp": "2026-01-01", "value": 42.5, "unit": "Celsius"}))
print(transform_record({"timestamp": "2026-01-01", "value": "98.6", "unit": "F"}))고급 패턴 테크닉
AS 패턴: 전체 매치값 바인딩
as를 사용하면 구조 분해를 하면서도 매칭된 전체 값을 함께 캡처할 수 있습니다:
def process_item(item):
match item:
case {"name": str(name), "price": float(price)} as product if price > 100:
print(f"Premium product: {product}")
case {"name": str(name), "price": float(price)} as product:
print(f"Standard product: {product}")패턴 타입 조합하기
패턴은 자연스럽게 조합됩니다. 시퀀스/매핑/클래스 패턴을 섞을 수 있습니다:
@dataclass
class Order:
items: list
customer: dict
def process_order(order):
match order:
case Order(items=[single_item], customer={"vip": True}):
print(f"VIP single-item order: {single_item}")
case Order(items=[_, _, *rest], customer={"name": name}) if len(rest) > 0:
print(f"{name} ordered {2 + len(rest)} items")
case Order(items=[], customer=_):
print("Empty order")Jupyter Notebook에서 match-case 사용하기
패턴 매칭은 데이터 분석 워크플로우에서 서로 다른 데이터 형식이나 API 응답을 처리해야 할 때 특히 유용합니다. Jupyter notebook에서 데이터셋을 인터랙티브하게 탐색할 때, match-case는 마주치는 다양한 데이터 “형태(shape)”를 깔끔하게 처리하는 방법을 제공합니다.
def classify_cell_output(output):
"""Classify Jupyter cell output by type."""
match output:
case {"output_type": "stream", "text": text}:
return f"Text output: {len(text)} chars"
case {"output_type": "error", "ename": name, "evalue": value}:
return f"Error: {name}: {value}"
case {"output_type": "display_data", "data": {"image/png": _}}:
return "Image output"
case {"output_type": "execute_result", "data": {"text/html": html}}:
return "HTML table output"
case _:
return "Unknown output type"노트북에서 패턴 매칭과 기타 Python 3.10+ 기능을 쓰면서 AI 도움을 받고 싶은 데이터 사이언티스트라면, RunCell (opens in a new tab)은 Jupyter 안에서 바로 사용할 수 있는 AI agent를 제공하며, 실제 데이터 구조에 맞춰 match-case 문을 작성/디버깅/최적화하는 데 도움을 줍니다.
자주 하는 실수와 피하는 방법
실수 1: Python 3.10 미만에서 match-case 사용
# This fails with SyntaxError on Python 3.9 and earlier
match value:
case 1:
print("one")해결: sys.version_info >= (3, 10)을 확인하거나 Python을 업그레이드하세요.
실수 2: case 이름을 비교로 착각하기
expected = 200
match status_code:
case expected: # WRONG: creates a new variable 'expected'
print("OK")해결: 점 표기, 리터럴, 또는 가드를 사용하세요:
match status_code:
case code if code == expected:
print("OK")실수 3: 와일드카드 케이스를 빼먹기
case _가 없으면 매칭되지 않은 값은 조용히 아무 일도 하지 않습니다:
match command:
case "start":
run()
case "stop":
halt()
# If command is "pause", nothing happens -- no error, no warning해결: 예기치 않은 입력을 명시적으로 처리하기 위해 와일드카드 케이스를 포함하세요:
match command:
case "start":
run()
case "stop":
halt()
case _:
raise ValueError(f"Unknown command: {command}")실수 4: 구체성이 없는 순서 의존 패턴
패턴은 위에서 아래로 검사됩니다. 넓은 패턴이 먼저 나오면 구체적인 패턴을 가려버립니다:
# WRONG: the first case always matches
match point:
case (x, y): # Captures everything
print("generic")
case (0, 0): # Never reached
print("origin")해결: 구체적인 패턴을 먼저 배치하세요:
match point:
case (0, 0):
print("origin")
case (x, y):
print(f"point at ({x}, {y})")성능 고려사항
match-case는 효율적인 바이트코드로 컴파일됩니다. 단순 리터럴 매칭에서는 Python 컴파일러가 이를 딕셔너리 조회와 유사한 방식으로 최적화합니다. 구조적 패턴의 경우, 중복 체크를 피하는 결정 트리(decision tree)를 생성합니다.
벤치마크에 따르면 match-case는 케이스 수가 적을 때(5개 미만) if-elif 체인과 성능이 비슷합니다. 하지만 디스패치 테이블이 커질수록(10개 이상) 바이트코드 최적화 덕분에 match-case가 더 빠를 수 있으며, 특히 리터럴 패턴에서 유리합니다.
다만 단순히 값에 따라 함수를 매핑하기만 하면, 패턴 매칭은 딕셔너리 디스패치를 대체하지 않습니다:
# For simple value-to-action mapping, a dict is cleaner and faster
handlers = {
"click": handle_click,
"scroll": handle_scroll,
"keypress": handle_keypress,
}
handler = handlers.get(event_type, handle_unknown)
handler(event_data)구조 검사, 구조 분해, 가드 조건처럼 딕셔너리 조회로는 표현하기 어려운 요구가 있을 때 match-case를 사용하세요.
FAQ
match-case를 쓰려면 Python 버전이 무엇이 필요하나요?
match-case 문은 Python 3.10 이상이 필요합니다. 이 기능은 2021년 10월 Python 3.10 릴리스의 일부로 PEP 634를 통해 도입되었습니다. Python 3.9 이하에서 match-case를 사용하면 SyntaxError가 발생합니다. 터미널에서 python --version을 실행해 버전을 확인할 수 있습니다.
Python match-case는 다른 언어의 switch-case와 같은가요?
아니요. Python의 match-case는 전통적인 switch-case보다 강력한 구조적 패턴 매칭입니다. C나 Java의 switch-case가 값 비교만 하는 반면, Python의 match-case는 시퀀스와 딕셔너리를 구조 분해하고, 클래스 인스턴스를 속성 기반으로 매칭하며, 매칭된 데이터에서 변수를 바인딩하고, 가드 조건까지 적용할 수 있습니다. Rust, Scala, Haskell의 패턴 매칭에 더 가깝습니다.
match-case에는 C switch처럼 fall-through가 있나요?
아니요. Python의 match-case는 처음 매칭되는 case 블록만 실행한 뒤 match 문을 빠져나옵니다. fall-through 동작이 없고 break 문도 필요하지 않습니다. 여러 패턴에서 같은 코드를 실행하고 싶다면 파이프 연산자를 사용한 or-패턴을 쓰면 됩니다. 예: case "a" | "b" | "c":는 이 값들 중 어떤 것이든 매칭합니다.
match-case를 정규표현식과 함께 사용할 수 있나요?
직접적으로는 불가능합니다. match-case의 패턴은 정규표현식 기반이 아니라 구조 기반입니다. 하지만 가드 절을 사용해 정규표현식 매칭을 적용할 수는 있습니다: case str(s) if re.match(r"pattern", s):는 구조적 타입 체크와 가드의 정규식 검증을 결합합니다.
match-case는 None 값을 어떻게 처리하나요?
None은 case None:이라는 리터럴 패턴으로 매칭됩니다. None은 Python의 싱글턴이므로 match는 동일성 비교(즉 is None과 동등)를 사용합니다. 즉 case None:은 실제 None 객체에만 매칭되며, 0, False, 빈 문자열 같은 다른 falsy 값에는 매칭되지 않습니다.
어떤 case도 매칭되지 않으면 어떻게 되나요?
매칭되는 case가 없고 와일드카드 패턴(case _:)도 없다면, match 문은 아무 동작도 하지 않은 채로 조용히 넘어가며(match 블록 이후로 실행이 계속됨) 예외도 발생하지 않습니다. 그래서 일반적으로 기본 동작을 제공하거나 예기치 않은 값을 명시적으로 처리하기 위해 와일드카드 케이스를 포함하는 것이 좋습니다.
결론
Python의 match-case 문은 복잡한 분기 로직에서 오랫동안 if-elif 체인에 의존해 온 언어에 구조적 패턴 매칭을 도입했습니다. 데이터 형태를 검사하고, 시퀀스와 딕셔너리를 구조 분해하며, 객체 타입에 따라 디스패치하고, 변수를 바인딩하는 일을—동등한 조건문 코드보다 더 명확하게 읽히는 문법으로—선언적으로 표현할 수 있게 해줍니다.
핵심은 match-case가 단순한 switch 문이 아니라는 점입니다. match-case는 구조화된 데이터를 다루기 위한 도구입니다. 커맨드 파싱, API 응답 처리, 상태 머신 구현, 객체 타입 기반 라우팅처럼 “값 비교”보다 “데이터의 형태”를 확인해야 하는 분기에서는 match-case가 더 짧고 유지보수하기 쉬운 코드를 만들어 줍니다.
처음에는 간단한 리터럴 패턴에서 시작한 뒤, 필요에 따라 시퀀스 분해, 매핑 패턴, 클래스 패턴을 점진적으로 도입하세요. 예기치 않은 입력을 명시적으로 처리하기 위해 와일드카드 케이스를 항상 포함하고, 구체적인 패턴을 일반적인 패턴보다 앞에 두며, bare name은 비교가 아니라 값 캡처를 한다는 점을 기억하세요.
자신의 데이터로 match-case 패턴을 인터랙티브하게 탐색하려면 Jupyter notebook이 패턴을 단계적으로 테스트하기에 이상적인 환경입니다. RunCell (opens in a new tab)은 Python 3.10+ 기능을 이해하는 AI 지원을 더해, 데이터 형태에 맞는 패턴 구조를 제안하며 이 워크플로우를 강화합니다.