프로그램을 만들다 보면 언젠가는 빨간 글씨를 만나게 됩니다. 처음에는 그 화면이 무섭습니다. 하지만 조금만 익숙해지면 생각이 바뀝니다. 오류 메시지는 나를 혼내는 문장이 아니라, 어디가 막혔는지 알려 주는 힌트입니다.
이번 장에서는 오류를 겁내지 않고 읽는 법부터 배웁니다. 그리고 try, except를 사용해 프로그램이 갑자기 멈추지 않도록 만드는 방법까지 익혀 보겠습니다. 이 장을 잘 넘기면, 초보자가 가장 자주 멈추는 구간을 훨씬 수월하게 지나갈 수 있습니다.
오류는 크게 두 종류다
파이썬은 문제를 크게 두 부류로 보여 줍니다. 하나는 문법 오류이고, 다른 하나는 예외입니다.1
문법 오류는 파이썬 문장의 규칙을 어겼을 때 생깁니다. 예를 들어 콜론 :을 빼먹거나, 괄호를 닫지 않거나, 들여쓰기가 어긋나면 프로그램은 시작도 하기 전에 멈춥니다.
def greet()
print("안녕하세요")이 코드는 함수 정의 뒤의 콜론이 없어서 실행되지 않습니다.
반면 예외는 문법은 맞지만, 실행하는 도중에 문제가 생길 때 나타납니다.
print(10 / 0)이 코드는 문법상 틀리지 않습니다. 하지만 0으로 나누는 순간 ZeroDivisionError가 발생합니다.
정리하면 이렇습니다.
문법 오류: 파이썬이 코드를 이해하지 못해서 시작도 못 함
예외: 코드는 이해했지만 실행 중에 문제가 생김
여기서 한 가지 더 기억하면 좋은 점이 있습니다. 모든 버그가 오류 메시지로 나타나는 것은 아닙니다. 프로그램은 멀쩡히 실행되지만 결과가 틀릴 수도 있습니다. 이런 경우는 보통 논리 오류라고 부릅니다. 즉, 빨간 글씨가 없다고 해서 항상 정답은 아니라는 뜻입니다.
에러 메시지는 아래에서부터 읽자
오류 메시지가 길게 나오면 초보자는 위에서부터 읽으려는 경우가 많습니다. 하지만 트레이스백은 아래에서부터 읽는 습관이 훨씬 도움이 됩니다.2
예를 들어 이런 코드가 있다고 해 봅시다.
numbers = [10, 20, 30]
print(numbers[5])실행하면 대략 이런 메시지를 보게 됩니다.
Traceback (most recent call last):
File "example.py", line 2, in <module>
print(numbers[5])
IndexError: list index out of range이 메시지는 이렇게 읽으면 됩니다.
맨 마지막 줄을 봅니다.
IndexError: list index out of range는 "리스트에서 없는 위치를 꺼내려 했다"는 뜻입니다.그 위 줄을 봅니다.
어느 파일의 몇 번째 줄에서 문제가 났는지 알려 줍니다.더 위쪽은 흐름을 보여 줍니다.
함수가 여러 번 호출되었다면, 어떤 순서로 여기까지 왔는지 알려 줍니다.
또 하나 중요한 점은, 화살표나 표시가 가리키는 위치가 항상 진짜 원인과 정확히 일치하지는 않는다는 것입니다. 파이썬이 "여기쯤에서 이상함을 발견했다"고 알려 주는 경우도 있기 때문입니다.1
오류 메시지를 볼 때는 먼저 이렇게 질문해 보세요.
"무슨 종류의 오류지?"
그다음 이렇게 이어 가면 됩니다.
"몇 번째 줄에서 났지? 그 줄 바로 앞에서 무엇을 했지?"
이 두 질문만 잘해도 디버깅 속도가 꽤 빨라집니다.
초보자가 자주 만나는 예외들
초보자가 특히 자주 만나는 예외는 몇 가지가 반복됩니다.3
NameError: 아직 만들지 않은 변수 이름을 썼을 때 생깁니다.print(score)TypeError: 서로 맞지 않는 자료형끼리 연산했을 때 생깁니다.print("10" + 5)ValueError: 자료형은 맞지만 값이 적절하지 않을 때 생깁니다.age = int("열살")ZeroDivisionError: 0으로 나눌 때 생깁니다.print(10 / 0)IndexError: 리스트나 튜플에서 없는 위치를 꺼낼 때 생깁니다.numbers = [1, 2, 3] print(numbers[3])KeyError: 딕셔너리에 없는 키를 꺼낼 때 생깁니다.student = {"name": "민지"} print(student["age"])FileNotFoundError: 없는 파일을 열려고 할 때 생깁니다.open("없는파일.txt")
이름만 보면 어렵게 느껴질 수 있지만, 사실 대부분은 아주 솔직한 영어입니다.
NameError는 이름 문제, TypeError는 자료형 문제, ValueError는 값 문제입니다. 이름을 곧 뜻으로 연결하는 습관을 들이면 오류가 훨씬 덜 무섭습니다.
try와 except의 가장 기본적인 모습
예외가 생겼다고 해서 프로그램이 무조건 끝나야 하는 것은 아닙니다. 우리가 예상한 문제라면, 그 상황에 맞는 반응을 직접 적어 줄 수 있습니다. 이때 쓰는 문법이 try와 except입니다.1
try:
age = int(input("나이를 입력하세요: "))
print(f"입력한 나이는 {age}살입니다.")
except ValueError:
print("숫자로 입력해 주세요.")이 코드는 이렇게 동작합니다.
먼저 try 안의 코드를 실행합니다. 사용자가 20을 입력하면 아무 문제 없이 지나가고, except는 실행되지 않습니다. 하지만 사용자가 스무 살처럼 숫자로 바꿀 수 없는 값을 입력하면 ValueError가 발생하고, 파이썬은 곧장 except ValueError: 블록으로 이동합니다.
여기서 중요한 점은 예외가 발생한 뒤에는 try 블록의 남은 코드가 계속 실행되지 않는다는 것입니다. 문제를 만난 순간, 그 아래 줄은 건너뛰고 except로 점프합니다.
반복문과 함께 쓰면 정말 강력하다
입력을 한 번 받고 끝내는 대신, 올바른 값을 받을 때까지 계속 물어보게 만들 수도 있습니다.
while True:
try:
number = int(input("정수를 입력하세요: "))
print(f"입력 성공: {number}")
break
except ValueError:
print("정수가 아닙니다. 다시 입력해 주세요.")이 예제는 초보자에게 아주 중요합니다. 프로그램이 한 번의 실수 때문에 바로 끝나지 않고, 다시 기회를 주기 때문입니다. 사용자 입장에서도 훨씬 친절한 프로그램이 됩니다.
try와 except는 마치 문지기 같습니다. 문제가 생기면 문 앞에서 한 번 받아 주는 것입니다.
여러 예외를 나눠서 처리하기
현실의 프로그램은 한 가지 문제만 생기지 않습니다. 숫자를 잘못 입력할 수도 있고, 0으로 나눌 수도 있습니다. 이럴 때는 except를 여러 개 둘 수 있습니다.1
try:
a = int(input("첫 번째 숫자: "))
b = int(input("두 번째 숫자: "))
print(a / b)
except ValueError:
print("숫자를 정확히 입력해 주세요.")
except ZeroDivisionError:
print("0으로는 나눌 수 없어요.")이 코드가 좋은 이유는, 오류 종류에 따라 안내 문장이 달라지기 때문입니다. 숫자를 잘못 입력한 사람에게 "0으로 나눌 수 없어요"라고 말하면 어색하겠죠. 예외를 구분해서 처리하면, 프로그램의 말투가 더 정확해집니다.
같은 방식으로 처리하고 싶은 예외는 묶을 수도 있습니다.
try:
value = int(input("숫자 입력: "))
print([10, 20, 30][value])
except (ValueError, IndexError):
print("입력이 잘못되었어요.")다만 초보자라면 처음에는 예외를 구분해서 하나씩 적는 연습을 더 추천합니다. 그래야 어떤 상황에서 어떤 예외가 나는지 감이 빨리 생깁니다.
except Exception은 만능 같지만 조심해야 한다
가끔은 이런 코드를 보게 됩니다.
try:
something()
except Exception:
print("오류가 발생했습니다.")이 코드는 넓게 보면 편리합니다. 많은 예외를 한 번에 받을 수 있기 때문입니다. 하지만 너무 넓게 잡으면, 정말 중요한 버그까지 한꺼번에 덮어 버릴 수 있습니다.1
초보자일수록 다음 원칙을 기억하면 좋습니다.
예상한 예외만 구체적으로 잡는다.
예를 들어 int() 변환 때문에 생길 수 있는 문제를 다루는 중이라면 ValueError를 잡는 편이 좋습니다. 그냥 무조건 Exception으로 덮어 버리면, 내가 생각하지 못한 다른 문제까지 숨어 버릴 수 있습니다.
else와 finally를 알면 코드가 더 깔끔해진다
try 문에는 except만 있는 것이 아닙니다. else와 finally도 함께 쓸 수 있습니다.1
먼저 else는 예외가 없을 때만 실행됩니다.
try:
number = int(input("정수를 입력하세요: "))
except ValueError:
print("정수를 입력해야 합니다.")
else:
print(f"입력한 값은 {number}입니다.")이 구조가 좋은 이유는 "위험한 코드"와 "성공했을 때 할 일"을 분리할 수 있기 때문입니다. try 안에 너무 많은 코드를 넣으면, 어떤 줄에서 난 오류를 잡고 있는지 헷갈리기 쉽습니다.
그리고 finally는 예외가 있든 없든 항상 실행됩니다.
try:
number = int(input("정수를 입력하세요: "))
except ValueError:
print("정수를 입력해야 합니다.")
finally:
print("입력 처리를 마칩니다.")finally는 청소 담당이라고 생각하면 쉽습니다. 파일을 닫거나, 연결을 정리하거나, 마지막 안내 문구를 출력할 때 자주 씁니다. 다음 장에서 파일을 다룰 때 이 역할이 더 또렷하게 느껴질 것입니다.
함수 안에서 생긴 예외도 밖에서 잡을 수 있다
예외는 꼭 현재 줄에서만 처리되는 것이 아닙니다. try 안에서 호출한 함수 내부에서 문제가 생겨도, 바깥에서 잡을 수 있습니다.1
def divide(a, b):
return a / b
try:
result = divide(10, 0)
print(result)
except ZeroDivisionError:
print("함수 안에서 0으로 나누기가 발생했어요.")이 예제를 보면 try 블록 안에서 직접 10 / 0을 쓰지 않았는데도 예외를 잡을 수 있습니다. 파이썬은 함수 호출 흐름을 따라가다가, 맞는 except를 만나면 거기서 처리합니다.
그래서 트레이스백이 여러 줄로 길게 나오는 것입니다. "어떤 함수에서 시작해서 어디까지 왔는가"를 보여 주는 지도라고 생각하면 됩니다.
raise로 일부러 예외를 만들 수 있다
지금까지는 파이썬이 스스로 예외를 만드는 경우를 봤습니다. 그런데 때로는 우리가 직접 "이 값은 허용하면 안 된다"라고 말하고 싶을 때가 있습니다. 그럴 때 raise를 사용합니다.1
def set_age(age):
if age < 0:
raise ValueError("나이는 0보다 작을 수 없습니다.")
return age
print(set_age(20))이 함수는 20일 때는 잘 작동합니다. 하지만 -3을 넣으면 직접 ValueError를 발생시킵니다.
print(set_age(-3))이렇게 하면 함수 사용자가 잘못된 값을 넣었을 때, 문제를 빨리 알아차릴 수 있습니다. 특히 함수를 만들 때 raise는 아주 유용합니다. 함수가 아무 값이나 조용히 받아들이는 것보다, 잘못된 입력을 분명하게 거절하는 편이 더 안전하기 때문입니다.
예외 객체를 변수에 담아 보기
예외 뒤에 as를 붙이면 실제 오류 정보를 변수로 받을 수 있습니다.
try:
number = int(input("숫자 입력: "))
except ValueError as err:
print("입력이 잘못되었습니다.")
print("오류 내용:", err)이렇게 하면 사용자에게는 친절한 문장을 보여 주고, 개발자인 나는 구체적인 오류 내용을 확인할 수 있습니다. 초반에는 무조건 복잡한 메시지를 보여 줄 필요는 없지만, 디버깅할 때는 꽤 도움이 됩니다.
초보자가 자주 하는 예외 처리 실수
예외 처리는 강력하지만, 잘못 쓰면 오히려 디버깅을 더 어렵게 만들기도 합니다. 특히 다음 실수는 자주 나옵니다.
except:만 써서 모든 문제를 한꺼번에 덮어 버리기except Exception: pass처럼 오류를 조용히 무시해 버리기try블록 안에 너무 많은 코드를 넣어서 어디서 오류가 났는지 흐리게 만들기구체적인 예외보다 넓은 예외를 먼저 잡아서 뒤의
except가 의미 없어지게 만들기
예를 들어 아래 코드는 좋지 않은 예입니다.
try:
age = int(input("나이: "))
print(100 / age)
except:
print("문제가 생겼습니다.")이 코드는 숫자를 잘못 입력한 것인지, 0을 넣은 것인지, 다른 문제인지 구분이 되지 않습니다.
아래처럼 바꾸면 훨씬 좋습니다.
try:
age = int(input("나이: "))
print(100 / age)
except ValueError:
print("나이는 숫자로 입력해 주세요.")
except ZeroDivisionError:
print("0은 사용할 수 없습니다.")오류를 감춘다고 프로그램이 튼튼해지는 것은 아닙니다. 오히려 어디가 아픈지 못 찾게 되는 경우가 많습니다.
디버깅할 때 바로 써먹는 작은 습관
오류를 만났을 때 머리가 하얘지는 것은 자연스러운 일입니다. 그럴 때는 다음 순서대로 차분히 보면 됩니다.
마지막 줄에서 예외 이름을 확인합니다.
ValueError인지,TypeError인지부터 봅니다.줄 번호를 확인합니다.
표시된 줄과 그 바로 앞줄을 함께 봅니다.변수의 실제 값을 확인합니다.
필요하면print(x)나print(type(x), x)를 찍어 봅니다.코드를 줄여 봅니다.
큰 프로그램 전체를 보지 말고, 문제가 나는 부분만 작은 예제로 떼어 냅니다.
예를 들어 input()으로 받은 값은 항상 문자열이라는 사실을 잊으면 TypeError와 ValueError를 자주 만나게 됩니다. 이런 기본 사실을 하나씩 몸에 익히는 것이 디버깅 실력을 키우는 가장 빠른 길입니다.
작은 실전 예제: 안전한 계산기
지금까지 배운 내용을 한 번에 묶어 보겠습니다.
def safe_divide():
try:
a = float(input("첫 번째 숫자: "))
b = float(input("두 번째 숫자: "))
except ValueError:
print("숫자를 입력해야 합니다.")
return
try:
result = a / b
except ZeroDivisionError:
print("0으로는 나눌 수 없습니다.")
else:
print(f"결과: {result}")
finally:
print("계산을 종료합니다.")
safe_divide()이 코드에서 눈여겨볼 점은 try를 두 번 나눴다는 것입니다. 첫 번째 try는 입력을 숫자로 바꾸는 일만 맡고, 두 번째 try는 나누기 계산만 맡습니다. 이렇게 하면 어떤 단계에서 문제가 생겼는지 더 분명하게 보입니다.
이 방식은 초보자에게 특히 좋습니다. try를 크게 하나로 감싸는 것보다, "어디가 위험한가"를 나눠서 생각하게 도와주기 때문입니다.
마무리
오류를 잘 읽는 사람은 코드를 잘 고치는 사람입니다. 그리고 예외를 잘 다루는 사람은 프로그램을 더 친절하고 안전하게 만드는 사람입니다.
이번 장에서 꼭 기억할 핵심은 세 가지입니다.
오류 메시지는 겁낼 대상이 아니라 힌트다.
트레이스백은 아래에서부터 읽는다.
try,except,else,finally를 사용하면 프로그램이 더 단단해진다.
처음에는 예외 이름이 낯설고, 빨간 글씨가 나오면 마음이 급해질 수 있습니다. 하지만 몇 번만 차분히 읽어 보면 어느 순간부터는 오류 메시지가 마치 선생님처럼 보이기 시작합니다.
다음 장에서는 파일을 읽고 쓰는 방법과 모듈을 사용하는 법을 배웁니다. 그 과정에서도 예외 처리는 아주 중요한 역할을 하게 됩니다. 특히 파일이 없거나 경로가 잘못되었을 때, 오늘 배운 내용이 큰 힘이 될 것입니다.
참고
1Python 3.14 문서: Errors and Exceptions
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.