파이썬 예외 처리: try, except, else, finally, raise와 사용자 정의 예외
파이썬 예외는 실행을 멈추게 하는 오류 객체입니다. try/except로 잡고, else는 오류가 없을 때, finally는 항상 실행됩니다. raise 문으로 직접 발생시킬 수 있으며(예: raise ValueError("금액이 음수입니다")), 사용자 정의 예외는 Exception을 상속한 클래스로 만듭니다.
파이썬 예외는 오류가 났다는 신호를 나타내는 객체이며, 평소의 실행 흐름을 멈춥니다. 아무도 잡지 않으면 파이썬은 트레이스백을 출력하고 프로그램을 종료합니다. 예외는 try/except 구문으로 잡고, raise 문으로 직접 발생시킵니다. 이 글에서는 동작 방식, 자주 나오는 오류 종류, 그리고 예외 처리가 오류를 숨기는 일이 되지 않도록 하는 방법을 정리합니다.
파이썬 예외란
연산을 끝낼 수 없을 때 — 0으로 나누기, 없는 키 조회, 글자를 숫자로 바꾸기 — 파이썬은 예외 객체를 만들어 '던집니다'. 현재 함수의 실행은 멈추고, 예외는 호출 체인을 따라 올라가며 적절한 핸들러를 찾습니다. 찾지 못하면 프로그램이 종료됩니다.
>>> 10 / 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
자주 묻는 질문에 답하면, 파이썬에서 0으로 나누면 ZeroDivisionError가 발생합니다. /, //, % 모두 해당합니다.
트레이스백 읽는 법
트레이스백은 프로그램이 오류까지 어떻게 도달했는지 보여 주는 보고서입니다. 아래에서 위로 읽는 것이 편합니다. 마지막 줄이 가장 중요합니다. 예외의 종류와 메시지가 나옵니다. 그 위에는 오류가 난 코드 줄이 있고, 더 위에는 그 함수를 호출한 곳이 있습니다. 시작점까지 이런 식으로 이어집니다.
Traceback (most recent call last):
File "tracker.py", line 9, in <module>
total = parse_amount(row)
File "tracker.py", line 4, in parse_amount
return float(raw.strip())
~~~~~^^^^^^^^^^^^^
ValueError: could not convert string to float: '백'
Python 3.11부터는 오류가 난 코드 줄 아래에 ~~~^^^ 표시가 붙습니다(PEP 657). 어느 부분 식이 깨졌는지 정확히 보여 줍니다. 한 줄에 함수 호출이 여러 번 있을 때 특히 도움이 됩니다.
'Did you mean' 힌트
Python 3.10부터는 이름을 잘못 쓰면 비슷한 이름을 제안합니다. Python 3.15에서는 힌트가 더 똑똑해졌습니다. 다른 언어의 습관도 알아봅니다. list.push를 호출하면 append를, "text".toUpperCase()를 호출하면 upper를 제안합니다. 힌트는 중첩된 필드까지 살핍니다. 딕셔너리가 객체의 totals 속성에 있는데 객체에서 바로 items()를 호출하면 .totals.items를 제안합니다. 가까운 이름은 delattr()에서도 제안됩니다.
파이썬 예외의 종류
내장 예외는 수십 가지지만, 처음에는 다음 몇 가지를 자주 보게 됩니다.
| 예외 | 발생하는 경우 | 예시 |
|---|---|---|
NameError | 정의되지 않은 이름, 대부분 오타 | print(totl) |
TypeError | 해당 타입에 맞지 않는 연산 | "5" + 3 |
ValueError | 타입은 맞지만 값이 잘못됨 | int("백") |
KeyError | 딕셔너리에 없는 키 | d["missing"] |
IndexError | 리스트 범위를 벗어난 인덱스 | [1, 2][5] |
AttributeError | 객체에 없는 메서드나 필드 | [].push(1) |
ZeroDivisionError | 0으로 나누기 | 1 / 0 |
FileNotFoundError | 파일이 없음 | open("nope.txt") |
SyntaxError(와 그 하위인 IndentationError)는 코드를 읽는 단계에서 발생하며, 코드가 실행되기 전입니다. 그래서 같은 파일의 try로는 잡을 수 없습니다.
예외의 계층 구조
예외는 클래스이며, 계층 구조를 이룹니다. 최상위는 BaseException입니다. 그 아래에 일반적인 오류를 담는 Exception과 몇 가지 시스템 예외(KeyboardInterrupt, SystemExit, GeneratorExit)가 있습니다.
BaseException
├── KeyboardInterrupt
├── SystemExit
└── Exception
├── ArithmeticError
│ └── ZeroDivisionError
├── LookupError
│ ├── KeyError
│ └── IndexError
├── OSError
│ └── FileNotFoundError
├── TypeError
└── ValueError
계층 구조는 예외를 잡을 때 중요합니다. except LookupError는 KeyError와 IndexError를 모두 잡습니다. KeyboardInterrupt는 코드의 오류가 아닙니다. 사용자가 Ctrl+C를 눌렀다는 뜻입니다. Exception을 상속하지 않도록 설계된 이유는, except Exception이 프로그램 중단을 막지 않게 하기 위해서입니다.
예외 처리: try/except/else/finally
오류가 날 수 있는 코드는 try 블록에 넣습니다. except 블록에는 어떤 예외를 잡을지와 그때 할 일을 적습니다. 파이썬에서 예외 처리에 쓰는 블록이 바로 except입니다.
def parse_amount(raw):
try:
value = float(raw)
except ValueError as e:
print(f"줄을 건너뜁니다: {e}")
return None
else:
return value # 오류가 없었을 때만 실행
finally:
print("파싱 완료") # 항상 실행
print(parse_amount("250"))
출력 순서는 먼저 '파싱 완료', 그다음 250.0입니다. else 블록의 return은 값을 기억해 두고, 파이썬은 finally를 반드시 실행한 뒤 결과를 돌려줍니다. 그래서 finally는 파일 닫기, 연결 해제 같은 정리 작업에 믿고 쓸 수 있습니다.
finally 안에서 return을 쓰면 앞의 반환값을 덮어쓰고, 예외마저 삼켜 버릴 수 있습니다. Python 3.14부터는 이런 코드에 SyntaxWarning(PEP 765)이 나옵니다. finally에서는 return을 쓰지 마세요.
여러 예외를 한꺼번에 처리하기
예외 종류별로 여러 except 블록을 쓰거나, 괄호 튜플에 여러 타입을 넣을 수 있습니다.
try:
amount = int(data["amount"])
except KeyError:
amount = 0
except (ValueError, TypeError) as e:
print("잘못된 금액:", e)
amount = 0
블록은 위에서 아래로 검사되며, 처음 맞는 블록이 실행됩니다. 따라서 구체적인 타입을 일반적인 타입보다 위에 둡니다. Python 3.14(PEP 758)부터는 괄호 없이 except ValueError, TypeError:처럼 쓸 수 있습니다. 다만 as를 함께 쓸 때는 여전히 괄호가 필요합니다.
예외 메시지와 타입 확인하기
as e로 예외 객체를 받을 수 있습니다. str(e)는 메시지를, type(e).__name__은 타입 이름을 줍니다.
try:
int("백")
except ValueError as e:
print(type(e).__name__) # ValueError
print(e) # invalid literal for int() with base 10: '백'
traceback 모듈: 전체 오류 텍스트
오류를 잡되 전체 트레이스백은 로그나 보고서에 남기고 싶을 때가 있습니다. 이때는 traceback 모듈을 씁니다.
import traceback
try:
1 / 0
except ZeroDivisionError:
traceback.print_exc() # 트레이스백을 stderr에 출력
text = traceback.format_exc() # 같은 내용을 문자열로
logging 모듈로 로그를 남긴다면, except 안에서 logging.exception("계산에 실패했습니다")를 호출하세요. 트레이스백이 메시지에 자동으로 붙습니다.
파이썬 raise: 예외 직접 발생시키기
raise 문은 예외를 직접 던집니다. 함수가 주어진 데이터로는 작업을 수행할 수 없다고 알릴 때 씁니다.
def add_expense(amount):
if amount < 0:
raise ValueError(f"금액은 음수가 될 수 없습니다: {amount}")
...
add_expense(-5)
# ValueError: 금액은 음수가 될 수 없습니다: -5
raise는 함수가 아니라 문장이므로 뒤에 괄호를 붙이지 않습니다. 괄호 안에는 예외 생성자에 넘길 인자를 씁니다. 종류는 의미에 맞게 고르세요. 잘못된 값이면 ValueError, 잘못된 타입이면 TypeError입니다.
인자 없는 raise와 raise from
except 안에서 인자 없이 raise만 쓰면 현재 예외를 그대로 다시 던집니다. 로그만 남기고 처리는 호출한 쪽에 맡길 때 씁니다.
한 예외를 다른 예외로 바꿔 던질 때는 raise ... from e를 씁니다. 원래 오류가 체인에 남아서 트레이스백에 두 오류가 모두 나옵니다.
def load_config(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError as e:
raise RuntimeError(f"설정 파일이 없습니다: {path}") from e
add_note: 오류에 설명 덧붙이기
Python 3.11(PEP 678)부터 예외에 add_note()로 메모를 붙인 뒤 다시 던질 수 있습니다. 예외 타입은 그대로이고, 메모는 메시지 아래에 출력됩니다.
def parse(raw, line_no):
try:
return float(raw)
except ValueError as e:
e.add_note(f"파일 expenses.csv, {line_no}번째 줄")
raise
parse("백", 17)
# ValueError: could not convert string to float: '백'
# 파일 expenses.csv, 17번째 줄
파이썬 사용자 정의 예외 만들기
사용자 정의 예외는 업무 도메인의 오류를 표현할 때 씁니다. 예산 초과, 이미 결제된 주문, 차단된 사용자 같은 경우입니다. Exception을 상속한 클래스를 만들면 됩니다.
class BudgetExceeded(Exception):
def __init__(self, category, limit, spent):
super().__init__(f"{category}: 한도 {limit}, 지출 {spent}")
self.category = category
self.limit = limit
self.spent = spent
def check_budget(category, limit, spent):
if spent > limit:
raise BudgetExceeded(category, limit, spent)
try:
check_budget("식비", 10000, 12500)
except BudgetExceeded as e:
print("경고:", e) # 경고: 식비: 한도 10000, 지출 12500
이제 호출하는 쪽은 예산 초과만 골라 잡을 수 있고, 오타나 다른 오류와 혼동하지 않습니다. 큰 프로젝트에서는 공통 기반 클래스(class TrackerError(Exception))를 만들고 모든 사용자 정의 예외가 이를 상속하게 하면, except TrackerError 하나로 모두 잡을 수 있습니다. BaseException이 아니라 Exception을 상속하세요.
예외 그룹과 except*
여러 오류가 동시에 생기기도 합니다. 예를 들어 파일 전체를 검사하거나, asyncio.TaskGroup으로 병렬 작업을 돌릴 때입니다. Python 3.11부터는 이런 오류를 ExceptionGroup으로 묶고, except*로 타입별로 처리할 수 있습니다.
def check_all(rows):
errors = []
for r in rows:
try:
float(r)
except ValueError as e:
errors.append(e)
if errors:
raise ExceptionGroup("파일 오류", errors)
try:
check_all(["1", "a", "b"])
except* ValueError as eg:
print(f"잘못된 줄: {len(eg.exceptions)}개") # 잘못된 줄: 2개
모든 예외를 잡는 try except: 왜 하면 안 되나
가장 해로운 습관은 모든 것을 잡고 무시하는 except: pass입니다. 프로그램은 '죽지 않게' 되지만, 실제로는 문제를 알려 주지 않게 됩니다. 아무 타입도 없는 except는 KeyboardInterrupt와 SystemExit까지 잡으므로 Ctrl+C로 프로그램을 멈출 수도 없게 됩니다. except Exception: pass가 조금 낫긴 해도, 이름 오타나 논리 오류를 조용히 삼켜 버립니다.
예외를 올바르게 처리하는 방법은 다음과 같습니다.
- 처리할 수 있는 타입만 잡으세요.
except ValueError는 좋고, 모든 것을 잡는 것은 나쁩니다. try블록은 짧게 유지하세요. 그 오류가 날 수 있는 줄만 넣습니다.- 프로그램 최상위에서 넓게 잡는다면(
except Exception) 반드시 트레이스백을 기록하세요. - 단순한 분기에 예외를 쓰지 마세요. 간단한 경우는
try/except KeyError대신dict.get()이 낫습니다. - 자원 관리에는
with를 쓰세요. 오류가 나도 파일을 닫아 줍니다.
정리
파이썬 예외는 오류를 알리는 정상적인 방법이지, 숨길 이유가 아닙니다. 트레이스백은 아래에서 위로 읽고, try/except에서는 구체적인 타입을 잡으며, else는 '성공했을 때', finally는 '정리'를 위해 씁니다. raise로 직접 오류를 던지고, raise from과 add_note로 맥락을 남기고, 사용자 정의 예외는 Exception을 상속해 만드세요. 이 주제는 과정 강의에서 실습합니다. 지출 관리기가 CSV의 잘못된 한 줄 때문에 멈추지 않고, 예산 초과를 자체 예외로 표현하도록 만듭니다.
자주 묻는 질문
파이썬에서 오류와 예외는 무엇이 다른가요?
일상적으로는 같은 말입니다. 엄밀히는 SyntaxError는 코드를 읽는 단계에서 발생하고, 나머지 오류는 실행 중에 예외 객체로 발생하여 잡을 수 있습니다.
예외가 난 뒤 같은 자리에서 실행을 이어 갈 수 있나요?
아닙니다. 예외를 잡은 뒤에는 오류가 난 줄이 아니라 try/except 블록 뒤에서 실행이 이어집니다. 파일의 각 줄을 개별적으로 처리하고 싶다면 try를 반복문 안에 넣으세요.
except 블록 안에서 예외가 나면 어떻게 되나요?
새 예외가 위로 전파되며, 원래 예외는 새 예외의 __context__ 속성에 남습니다. 트레이스백에는 'During handling of the above exception, another exception occurred'라는 문장이 나타납니다.
테스트에서 함수가 예외를 던지는지 어떻게 확인하나요?
pytest에서는 with pytest.raises(ValueError):, unittest에서는 self.assertRaises(ValueError)를 씁니다. 블록 안에서 원하는 타입의 예외가 나야 테스트가 통과합니다.
assert와 raise는 무엇이 다른가요?
assert 조건, "메시지"는 조건이 거짓일 때 AssertionError를 발생시키며, 개발 중의 검사 용도입니다. -O 옵션으로 실행하면 assert가 꺼지므로, 사용자 입력은 if와 raise로 검사해야 합니다.