13  부록 B. 에러 메시지 읽는 법과 디버깅 습관

13.1 이 부록을 읽는 법

수업 3주차쯤이면 꼭 나오는 장면이 있습니다. 화면에 빨간 글자가 가득 뜨자마자 학생이 손을 들고 말합니다. “저 다 지우고 처음부터 다시 쓸게요.” 잠깐만요. 방금 지우려던 그 빨간 글자 안에, 몇 번째 줄에서 무엇이 왜 잘못됐는지가 전부 적혀 있었습니다. 지워야 할 것은 코드가 아니라, 빨간 글자는 꾸중이라는 오해입니다.

에러 메시지는 혼내는 것이 아니라 알려주는 것입니다. 파이썬은 멈출 수밖에 없었던 사정을 최대한 자세히 보고하려고 그 글자들을 띄웁니다. 초심자와 숙련자의 차이는 에러를 내는 횟수가 아닙니다. 오히려 숙련자가 더 많이 냅니다. 시도하는 코드의 양이 많으니까요. 차이는 에러가 났을 때 하는 일에 있습니다. 초심자는 코드를 노려보고, 숙련자는 메시지를 읽습니다.

이 부록은 세 부분입니다. 먼저 파이썬의 오류 보고서인 트레이스백(traceback)을 읽는 법을 익히고, 다음으로 이 책의 실습에서 여러분이 만날 오류의 구할을 덮는 대표 오류 여덟 종을 사전 형식으로 정리합니다. 마지막은 더 까다로운 상대, 즉 에러 없이 틀린 답을 내는 코드를 잡는 디버깅 습관입니다. 부록 A와 마찬가지로 처음부터 끝까지 정독하는 장이 아닙니다. 본문을 공부하다 빨간 글자를 만나면, 오류의 이름을 확인하고 이 사전에서 그 항목을 찾아 읽으세요.

13.2 트레이스백: 파이썬이 남기는 사고 보고서

오류가 나서 프로그램이 멈추면 파이썬은 트레이스백이라는 보고서를 남깁니다. 직역하면 “거슬러 올라가기”인데, 사고 지점에서 출발점까지 호출의 경로를 거슬러 보여준다는 뜻입니다. 겉모습은 위압적이지만 담긴 내용은 세 가지 질문에 대한 답이 전부입니다. 어디서 멈췄는가(셀과 줄 번호), 무엇이 잘못됐는가(오류의 종류), 잘못됐는가(구체적인 사정). 이 세 답을 찾아 읽는 것이 트레이스백 독해의 전부입니다.

실물을 놓고 이야기하겠습니다. 5장에서 하던 대로 이콘 카페의 하루 이익 계산을 함수 두 개로 짰습니다. 그런데 장부에서 재료비를 옮겨 적던 아르바이트생이 숫자를 그만 따옴표로 감쌌습니다. 어떤 보고서가 나오는지 보겠습니다.

menu = {"아메리카노": 2300, "카페라떼": 3200, "샌드위치": 4200}

def unit_margin(item, cost):
    return menu[item] - cost

def daily_profit(item, cost, cups):
    return unit_margin(item, cost) * cups

print(daily_profit("아메리카노", "1200", 120))

실행하면 이런 보고서가 옵니다. 셀 번호(In[1])는 여러분 화면에서 다른 숫자일 수 있습니다.

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[1], line 9
      5
      6 def daily_profit(item, cost, cups):
      7     return unit_margin(item, cost) * cups
      8
----> 9 print(daily_profit("아메리카노", "1200", 120))

Cell In[1], line 7, in daily_profit(item, cost, cups)
      6 def daily_profit(item, cost, cups):
----> 7     return unit_margin(item, cost) * cups

Cell In[1], line 4, in unit_margin(item, cost)
      3 def unit_margin(item, cost):
----> 4     return menu[item] - cost

TypeError: unsupported operand type(s) for -: 'int' and 'str'

한 줄씩 뜯어보기 — 이번에는 코드가 아니라 보고서를 뜯어봅니다.

  • TypeError Traceback (most recent call last) — 보고서의 표지입니다. 오류의 종류(무엇이)를 먼저 밝히고, 괄호 안에서 읽는 방향을 안내합니다. most recent call last, 즉 “가장 나중에 호출된 것이 맨 아래”라는 뜻입니다. 사건 현장은 아래쪽에 있습니다.
  • Cell In[1], line 9 — 문제의 출발점입니다. 셀의 9번 줄, 그러니까 print(daily_profit(...))에서 일이 시작됐습니다. ----> 화살표가 해당 줄을 가리키고, 주변 줄을 몇 개 함께 보여주어 맥락을 알 수 있게 합니다.
  • Cell In[1], line 7, in daily_profit(...) — 실행이 daily_profit 함수 안으로 들어갔고, 그 안의 7번 줄에서 다시 unit_margin을 불렀다는 기록입니다. 경로의 중간 기착지입니다.
  • Cell In[1], line 4, in unit_margin(...) — 마지막 기착지, 즉 사건 현장입니다. menu[item] - cost를 계산하다가 멈췄습니다.
  • TypeError: unsupported operand type(s) for -: 'int' and 'str' — 보고서의 결론()입니다. 빼기(-)라는 연산을 int와 str 사이에는 적용할 수 없다는 것. menu[item]은 메뉴판에서 꺼낸 가격이니 int이고, 그렇다면 str은 cost입니다. 재료비가 "1200"이라는 문자열로 들어온 것이 들통났습니다. 따옴표를 벗겨 1200으로 건네면 해결됩니다.

여기서 이 부록 전체를 통틀어 가장 중요한 규칙이 나옵니다. 트레이스백은 마지막 줄부터 읽습니다. 마지막 줄이 결론(무엇이·왜)이고, 그 바로 위 화살표가 사건 현장(어디서)이며, 더 위쪽은 현장까지의 경위입니다. 결론을 먼저 읽고, 필요한 만큼만 위로 올라가는 것입니다. 위에서부터 차례로 읽으면 결론에 닿기 전에 기가 꺾입니다.

보고서가 수십 줄일 때: 내 코드 프레임 찾기

9장에서 판다스를 쓰기 시작하면 보고서가 수십 줄로 길어집니다. 예컨대 read_csv에 잘못된 파일 이름을 건네면 파이썬은 판다스 내부 깊숙한 곳에서 멈추기 때문에, site-packages\pandas\... 같은 낯선 파일 경로의 기록이 줄줄이 등장합니다. 겁낼 것 없습니다. 규칙은 같습니다. 마지막 줄(FileNotFoundError: [Errno 2] No such file or directory: 'data/cafe_sale.csv' 같은 결론)을 먼저 읽고, 위로 올라가면서 Cell In[...]으로 시작하는 기록, 즉 내가 쓴 코드를 찾습니다. 사고가 남의 집(라이브러리 코드) 안에서 났어도 원인은 대부분 내가 건넨 값입니다. 방금 예시라면 마지막 줄에 인용된 파일 이름에서 sale이라는 오타를 발견하는 식입니다.

실수 노트 — 메시지를 읽지 않고 코드부터 바꾸는 습관

빨간 글자가 뜨면 시선을 돌리고, 어딘가를 대충 고쳐서 곧장 다시 실행하는 학생이 많습니다. 이 “무작위 수리”는 운이 좋으면 한 번에 끝나지만, 운이 나쁘면 멀쩡한 부분을 건드려 버그를 하나 잡는 동안 두 개를 심습니다. 더 나쁜 것은 고쳐졌어도 왜 고쳐졌는지 모른 채 지나간다는 점입니다. 같은 오류가 다음 주에 다시 옵니다. 규칙을 하나 정해 두세요. 코드에 손을 대기 전에, 트레이스백의 마지막 줄을 소리 내어 읽는다. 부록 A에서 셸 오류를 읽을 때 “인용된 단어를 소리 내어 읽으라”고 했던 것과 같은 원리입니다. 읽는 데 10초가 들고, 안 읽으면 10분이 듭니다.

13.3 대표 오류 사전: 여덟 항목

이제 사전입니다. 이 책의 실습에서 만나게 되는 오류는 대부분 아래 여덟 종류 안에 있습니다. 표의 두 번째 열만 훑어도 감이 올 텐데, 오류 이름은 파이썬이 하려는 말의 요약입니다.

오류 이름 파이썬이 하려는 말
SyntaxError 문법이 틀려서 문장을 읽을 수 없다
NameError 그런 이름은 들어본 적이 없다
TypeError 이 자료형끼리는 그 연산을 할 수 없다
IndexError 리스트에 그런 번호의 칸이 없다
KeyError 딕셔너리에 그런 키가 없다
IndentationError 들여쓰기가 문법과 어긋난다
ZeroDivisionError 0으로 나눌 수는 없다
ValueError 자료형은 맞는데 값이 받아들일 수 없다

항목마다 구성은 같습니다. 본문 실습에서 실제로 벌어질 법한 사고를 재현하고, 메시지를 어떻게 읽는지, 원인 후보는 무엇인지, 고친 코드는 어떤 모습인지 순서로 갑니다. 재현 셀은 실행하면 실제로 에러가 나는 셀이니, 직접 돌려 보며 빨간 글자를 눈에 익히는 것도 좋은 공부입니다. 참고로 여덟 중 SyntaxErrorIndentationError 두 형제는 성격이 조금 다릅니다. 파이썬은 코드를 실행하기 전에 문장 전체를 한 번 읽어 보는데, 이 검사에서 걸리는 것이 이 둘입니다. 실행이 아예 시작되지 않았으므로 호출 경로가 없고, 그래서 보고서도 짧습니다. 나머지 여섯은 실행 중에 나며, 앞에서 본 완전한 사고 보고서가 옵니다.

SyntaxError — 문장을 읽을 수 없습니다

3장에서 if 뒤의 콜론을 처음 배울 때, 빠뜨리면 SyntaxError가 난다고 예고해 두었습니다. 그 현장을 재현합니다. 누진세 구간 판정을 짜다가 콜론을 빠뜨렸습니다.

income = 30_000_000
if income > 14_000_000
    print("과세표준이 1구간을 넘습니다")
  Cell In[2], line 2
    if income > 14_000_000
                          ^
SyntaxError: expected ':'

이 메시지를 어떻게 읽는가. 실행 전 검사에서 걸렸으므로 보고서가 짧습니다. ^ 기호가 파이썬이 읽다가 막힌 지점을 가리키고, 마지막 줄이 기대했던 것을 직접 말해 줍니다. expected ‘:’, 콜론이 왔어야 한다는 뜻입니다. 요즘 파이썬(3.10 이후)의 SyntaxError는 이렇게 무엇이 빠졌는지, 무엇을 착각했는지까지 짚어 주는 경우가 많아서, 마지막 줄만 제대로 읽으면 수리가 끝나는 오류입니다.

원인 후보.

  1. if, elif, else, for, while, def 줄 끝의 콜론 누락. 방금 본 경우입니다.
  2. 비교하려던 자리에 대입을 쓴 경우. if price = 4000:이라고 치면 SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?라는 맞춤 안내가 옵니다. 3장에서 다룬 ===의 구분입니다.
  3. 괄호나 따옴표를 안 닫은 경우. 이때 파이썬은 다음 줄까지 읽고 나서야 멈추기 때문에 엉뚱한 줄이 지목되기도 하고, 여는 괄호 쪽을 지목하며 SyntaxError: '(' was never closed라고 알려주기도 합니다. 지목된 줄에서 잘못을 못 찾겠으면 바로 윗줄의 괄호와 따옴표 개수를 세어 보세요.

콜론을 채우면 문장이 읽힙니다.

income = 30_000_000
if income > 14_000_000:
    print("과세표준이 1구간을 넘습니다")
과세표준이 1구간을 넘습니다

NameError — 그런 이름은 모릅니다

실행 중에 나는 오류 가운데 가장 먼저, 그리고 가장 자주 만나는 것이 NameError입니다. 파이썬은 변수 이름을 글자 그대로, 한 글자도 에누리 없이 기억합니다. 2장에서 americano_price라는 이름을 지어 놓고 부를 때 철자가 하나라도 다르면, 사람 눈에는 같아 보여도 파이썬에게는 생판 남입니다.

americano_price = 2300
cups = 120
revenue = america_price * cups
print(f"오늘 매출: {revenue:,}원")
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 3
      1 americano_price = 2300
      2 cups = 120
----> 3 revenue = america_price * cups
      4 print(f"오늘 매출: {revenue:,}원")

NameError: name 'america_price' is not defined

이 메시지를 어떻게 읽는가. 따옴표 안에 인용된 이름이 용의자입니다. 그 이름을 소리 내어 읽고, 화살표 줄 위쪽에서 내가 정의한 이름과 한 글자씩 대조하세요. 정의한 이름은 americano_price, 부른 이름은 america_price. 두 글자가 빠졌습니다. 환경에 따라서는(요즘 파이썬을 터미널에서 쓸 때나 Colab에서) 마지막 줄 뒤에 Did you mean: 'americano_price'?라는 제안까지 붙습니다. 파이썬이 정답 후보를 알려주는 셈이니, 보이면 고맙게 받으면 됩니다.

원인 후보.

  1. 변수 이름의 오타. 정의할 때든 부를 때든 어느 한쪽이 틀렸습니다.
  2. 그 변수를 만드는 셀을 아직 실행하지 않은 경우. 노트북 특유의 함정이라 바로 아래 실수 노트에서 따로 다룹니다.
  3. 문자열에 따옴표를 빼먹은 경우. print(아메리카노)라고 쓰면 파이썬은 아메리카노라는 글자를 출력하는 대신 아메리카노라는 이름의 변수를 찾아 나서고, 없으니 NameError가 납니다.

이름을 맞춰 주면 끝납니다.

americano_price = 2300
cups = 120
revenue = americano_price * cups
print(f"오늘 매출: {revenue:,}원")
오늘 매출: 276,000원

실수 노트 — 어제는 되던 코드가 오늘 NameError를 냅니다

어제 저장할 때까지 멀쩡히 돌던 노트북을 오늘 다시 열어 중간 셀부터 실행하면 NameError가 납니다. 코드는 한 글자도 안 바뀌었는데 말이지요. 원인은 커널의 기억력입니다. 노트북의 계산 담당인 커널은 노트북을 닫으면(또는 Colab 접속이 끊기면) 기억이 백지가 되고, 화면에 남아 있는 어제의 출력은 기록일 뿐 기억이 아닙니다. 중간 셀의 revenue는 위쪽 셀들이 이번 세션에서 실행됐어야 존재합니다. 처방은 간단합니다. 위에서부터 차례로 다시 실행하는 것. 메뉴의 “Restart & Run All”(다시 시작 후 모두 실행)이 이 일을 한 번에 해 줍니다. 덤으로, 과제를 제출하기 전에 Restart & Run All이 처음부터 끝까지 오류 없이 도는지 확인하는 것을 의식처럼 삼으세요. 셀을 이 순서 저 순서로 실행하며 작업하다 보면, 위에서부터 순서대로는 돌지 않는 노트북이 되어 있는 경우가 흔합니다.

TypeError — 이 자료형끼리는 그 연산을 할 수 없습니다

2장에서 input()은 무엇을 입력받든 문자열을 돌려준다고 강조했습니다. 그 경고가 현실이 되는 자리가 여기입니다. 아메리카노를 사이즈업하면 500원이 붙는다고 합시다. 가격을 입력받아 500을 더합니다.

price_text = input("아메리카노 가격(원): ")   # 2300 을 입력
total = price_text + 500
print(f"사이즈업 결제액: {total:,}원")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[4], line 2
      1 price_text = input("아메리카노 가격(원): ")   # 2300 을 입력
----> 2 total = price_text + 500
      3 print(f"사이즈업 결제액: {total:,}원")

TypeError: can only concatenate str (not "int") to str

이 메시지를 어떻게 읽는가. TypeError의 마지막 줄에는 반드시 자료형의 이름이 등장하고, 그것이 단서의 전부이자 핵심입니다. str에는 str만 이어붙일(concatenate) 수 있는데 int를 이어붙이려 했다는 말을 뒤집으면, 파이썬은 +의 왼쪽인 price_text를 숫자가 아니라 문자열로 보고 있다는 뜻입니다. 그러니 진짜 질문은 “왜 price_text가 문자열이지?”가 되고, 답은 input()의 성질에 있습니다. 2300이라고 쳐도 도착하는 것은 "2300"입니다. TypeError 보고서는 잘못된 연산을 지적하는 척하면서, 사실은 변수에 예상 밖의 자료형이 들어 있음을 폭로하는 문서입니다. 어느 변수가 범인인지 아리송하면 print(type(price_text))로 자료형을 직접 물어보면 됩니다.

원인 후보.

  1. input()이 돌려준 문자열을 int()float()로 바꾸지 않고 계산에 넣은 경우.
  2. 숫자를 따옴표로 감싸 저장해 놓고 잊은 경우. 사전 첫머리의 사고 보고서 예제("1200")가 이 경우였습니다.
  3. 함수에 인자를 잘못된 순서로 건네서, 매개변수에 엉뚱한 자료형이 들어간 경우.

한 가지 경고를 덧붙입니다. 문자열과 숫자의 만남이 항상 TypeError로 끝나지는 않습니다. "2300" + "500"은 에러 없이 "2300500"이라는 문자열을 만들고, "15" * 3"151515"를 만듭니다. 파이썬 문법상 합법이기 때문입니다. 에러가 나는 조합보다 에러 없이 이상한 값을 만드는 조합이 더 위험합니다. 연습문제 B-3에서 이 함정을 직접 밟아 볼 것입니다.

노트북에서 실행할 수 있도록 input()이 돌려주었을 값을 변수에 직접 담고, 계산 전에 변환하는 것으로 고칩니다. 실제 코드라면 첫 줄을 price = int(input("아메리카노 가격(원): "))처럼 쓰면 됩니다.

price_text = "2300"          # input()이 돌려주었다고 가정한 값
price = int(price_text)      # 계산에 쓰기 전에 숫자로 변환
total = price + 500
print(f"사이즈업 결제액: {total:,}원")
사이즈업 결제액: 2,800원

IndexError — 리스트에 그런 칸이 없습니다

4장에서 리스트의 칸 번호는 0부터 시작한다고 배웠고, 그때부터 이 오류는 예약되어 있었습니다. 이콘 카페의 월별 판매량 12개를 리스트에 담고 12월 값을 꺼내려는 참입니다. “12월이니까 12번”이라는 생각이 사고를 부릅니다.

monthly_sales = [310, 285, 342, 360, 355, 298, 275, 330, 361, 372, 340, 388]
print("12월 판매량:", monthly_sales[12])
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[5], line 2
      1 monthly_sales = [310, 285, 342, 360, 355, 298, 275, 330, 361, 372, 340, 388]
----> 2 print("12월 판매량:", monthly_sales[12])

IndexError: list index out of range

이 메시지를 어떻게 읽는가. list index out of range, 리스트의 번호가 범위를 벗어났다는 뜻입니다. 이 메시지는 몇 번을 요구했는지는 말해 주지 않으므로, 화살표가 가리키는 줄에서 대괄호 안을 확인하는 것까지가 한 세트입니다. [12]를 요구했는데, 12칸짜리 리스트의 번호는 0번부터 11번까지입니다. 12번 칸은 없습니다.

원인 후보.

  1. 번호가 1부터 시작한다는 착각. 마지막 칸은 len(리스트) - 1번입니다.
  2. 반복 범위가 한 칸 넘친 경우. for i in range(1, 13): 안에서 monthly_sales[i]를 읽으면 마지막 바퀴에서 12번 칸을 요구하게 됩니다. 마지막 바퀴에서만 터지므로 “거의 다 돌고 나서 멈추는” 것이 특징입니다.
  3. 리스트가 비어 있는 경우. 빈 리스트에는 0번 칸조차 없어서 [0]부터 IndexError가 납니다. 조건에 맞는 값만 골라 담았는데 결과가 하나도 없었을 때 겪습니다.

12월은 11번 칸입니다. 그리고 4장에서 배운 음수 번호를 쓰면 “마지막 칸”을 셈 없이 지목할 수 있어 이런 실수 자체가 줄어듭니다.

monthly_sales = [310, 285, 342, 360, 355, 298, 275, 330, 361, 372, 340, 388]
print("12월 판매량:", monthly_sales[11])
print("12월 판매량(뒤에서 세기):", monthly_sales[-1])
12월 판매량: 388
12월 판매량(뒤에서 세기): 388

KeyError — 딕셔너리에 그런 키가 없습니다

KeyError는 IndexError의 형제입니다. 리스트에서 번호로 겪던 일을 딕셔너리에서 이름으로 겪는 것이지요. 5장의 이콘 카페 메뉴판으로 재현합니다. 메뉴판에는 "카페라떼"라고 붙여 썼는데, 주문을 처리하는 코드에는 "카페 라떼"라고 띄어 썼습니다.

menu = {"아메리카노": 2300, "카페라떼": 3200, "샌드위치": 4200}
print("카페 라떼 가격:", menu["카페 라떼"])
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[6], line 2
      1 menu = {"아메리카노": 2300, "카페라떼": 3200, "샌드위치": 4200}
----> 2 print("카페 라떼 가격:", menu["카페 라떼"])

KeyError: '카페 라떼'

이 메시지를 어떻게 읽는가. KeyError는 친절하게도 문제의 키를 마지막 줄에 그대로 인용해 줍니다. 그 키를 실제 딕셔너리의 키와 한 글자씩 대조하는 것이 독해의 전부인데, 함정은 띄어쓰기가 눈에 잘 안 띈다는 점입니다. ’카페 라떼’와 ’카페라떼’는 다른 문자열입니다. 대조가 어려우면 print(menu.keys())로 실제 키 목록을 출력해 놓고 비교하세요. 진단이 훨씬 빨라집니다.

원인 후보.

  1. 키의 철자·띄어쓰기 불일치. 방금 본 경우이고, "아메리카노 "처럼 끝에 공백이 딸린 키도 같은 부류입니다. 파일이나 입력에서 읽어 온 문자열에서 특히 잦습니다.
  2. 아직 추가하지 않은 키를 읽으려는 경우. 5장의 아이스티 주문이 그랬습니다.
  3. 키의 자료형이 다른 경우. 파일에서 읽은 "2024"(문자열)와 코드에 쓴 2024(정수)는 다른 키입니다. 9장에서 데이터를 다루기 시작하면 만나는 변형입니다.

메뉴판의 표기에 맞추고, 없을 수도 있는 키는 5장에서 배운 get으로 읽으면 프로그램을 멈추지 않고 기본값을 받을 수 있습니다.

menu = {"아메리카노": 2300, "카페라떼": 3200, "샌드위치": 4200}
print("카페라떼 가격:", menu["카페라떼"])
print("아이스티 가격:", menu.get("아이스티", 0), "(메뉴에 없어 기본값)")
카페라떼 가격: 3200
아이스티 가격: 0 (메뉴에 없어 기본값)

IndentationError — 들여쓰기도 문법입니다

다른 언어에서 들여쓰기는 예의지만 파이썬에서는 문법입니다. 3장에서 조건문 블록을 배우며 체득한 사실이지요. 콜론까지는 잘 쓰고 다음 줄을 들여 쓰지 않으면 이 오류가 납니다. 학식 예산 판정으로 재현합니다.

budget = 10000
lunch = 6500
if budget >= lunch:
print("오늘 점심은 학식입니다")
  Cell In[7], line 4
    print("오늘 점심은 학식입니다")
    ^
IndentationError: expected an indented block after 'if' statement on line 3

이 메시지를 어떻게 읽는가. SyntaxError의 사촌이라 역시 실행 전에 나고 보고서가 짧습니다. 그런데 마지막 줄이 유난히 구체적입니다. 3번 줄의 if 문 다음에 들여쓴 블록이 왔어야 한다. 무엇이(들여쓴 블록), 어디에(3번 줄의 if 다음) 빠졌는지까지 완성된 문장으로 말해 주니, 읽기만 하면 고치는 방법이 그대로 나옵니다.

원인 후보.

  1. 콜론으로 끝나는 줄 다음을 들여 쓰지 않은 경우. 방금 본 경우이고, if뿐 아니라 for, while, def 뒤에서도 똑같이 납니다.
  2. 반대로 들여 쓸 이유가 없는 줄을 들여 쓴 경우. 이때는 IndentationError: unexpected indent가 납니다. 코드를 복사해 붙여 넣다가 앞 공백이 딸려 온 흔적인 경우가 많습니다.
  3. 탭과 스페이스가 한 블록 안에 섞인 경우. TabError라는 전용 이름이 따로 있을 만큼 흔합니다. 화면에는 똑같은 공백으로 보여서 눈으로는 못 찾습니다. 웹 문서나 메신저에서 복사해 온 코드에서 잘 생기며, 문제의 블록을 지우고 손으로 다시 들여 쓰는 것이 가장 빠른 수리법입니다.

노트북 편집기는 콜론 뒤에서 엔터를 치면 자동으로 들여 써 주므로, 그 자동 들여쓰기를 지우지만 않으면 대부분 예방됩니다.

budget = 10000
lunch = 6500
if budget >= lunch:
    print("오늘 점심은 학식입니다")
오늘 점심은 학식입니다

ZeroDivisionError — 0으로 나눌 수 없습니다

수학의 규칙이 그대로 코드의 규칙이 된 경우입니다. 경제 계산에는 나눗셈이 유난히 많습니다. 성장률, 평균, 1인당 GDP, 탄력성이 모두 나눗셈이지요. 분모가 0이 될 수 있는 자리가 그만큼 많다는 뜻입니다. 이콘 카페가 작년에는 팔지 않던 쿠키를 올해 메뉴에 올렸다고 합시다. 쿠키 판매의 증가율을 습관처럼 계산하면 이렇게 됩니다.

last_year = 0        # 작년에는 쿠키를 팔지 않았다
this_year = 480
growth = (this_year - last_year) / last_year * 100
print(f"판매 증가율: {growth:.1f}%")
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[8], line 3
      1 last_year = 0        # 작년에는 쿠키를 팔지 않았다
      2 this_year = 480
----> 3 growth = (this_year - last_year) / last_year * 100
      4 print(f"판매 증가율: {growth:.1f}%")

ZeroDivisionError: division by zero

이 메시지를 어떻게 읽는가. 메시지 자체는 여덟 항목 중 가장 단순합니다. 0으로 나눴다, 그게 전부입니다. 그래서 독해의 무게는 다음 질문으로 옮겨 갑니다. 화살표 줄의 나눗셈에서 분모가 무엇이고, 그 값이 왜 0이었는가. 이 오류는 코드가 아니라 데이터가 범인인 대표적인 경우입니다. 수식 자체는 옳습니다. 다만 “작년 판매량이 0인 상품”이라는, 수식을 만들 때 상상하지 않았던 데이터가 들어온 것입니다.

원인 후보.

  1. 기준값이 0인 데이터. 신제품의 작년 판매량, 개업 첫 달의 전월 매출처럼 “이전 값”이 없는 경우입니다.
  2. 빈 리스트로 평균을 내는 경우. total / len(prices)에서 리스트가 비어 있으면 len이 0입니다.
  3. 변화분이 0인 분모. 탄력성 계산에서 가격이 그대로면 분모의 가격 변화율이 0이 됩니다.

수리는 계산 전에 분모를 검사하는 것입니다. 증가율이라는 개념 자체가 “이전 값이 있는 상품”에만 정의된다는 경제학의 사실을, 조건문이 코드에 새겨 넣는 셈입니다. 0이 들어올 수 있는 분모에는 조건문 한 겹을 두르세요.

last_year = 0
this_year = 480

if last_year == 0:
    print(f"작년 판매가 없어 증가율을 정의할 수 없습니다. 올해 판매량: {this_year}잔")
else:
    growth = (this_year - last_year) / last_year * 100
    print(f"판매 증가율: {growth:.1f}%")
작년 판매가 없어 증가율을 정의할 수 없습니다. 올해 판매량: 480잔

ValueError — 자료형은 맞는데 값이 틀렸습니다

사전의 마지막 항목은 TypeError와 짝을 이룹니다. TypeError가 자료형의 문제라면, ValueError는 자료형은 통과했는데 값이 문제인 경우입니다. int()는 문자열을 받도록 만들어져 있으니 int("2300")은 문제없지만, 같은 문자열이라도 내용이 숫자로 읽히지 않으면 값의 차원에서 거부됩니다. 국세청 화면에서 과세표준을 복사해 붙여 넣는 장면을 재현합니다. 화면의 금액에는 쉼표가 찍혀 있습니다.

income_text = "35,000,000"     # 화면에서 복사한, 쉼표가 든 금액
income = int(income_text)
print(f"과세표준: {income:,}원")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[9], line 2
      1 income_text = "35,000,000"     # 화면에서 복사한, 쉼표가 든 금액
----> 2 income = int(income_text)
      3 print(f"과세표준: {income:,}원")

ValueError: invalid literal for int() with base 10: '35,000,000'

이 메시지를 어떻게 읽는가. 마지막 줄을 끊어 읽으면 이렇습니다. int()가 받기에 유효하지 않은 글(literal)이었다, 십진수(base 10) 기준으로. 그리고 문제의 값이 따옴표 안에 그대로 인용됩니다. 인용된 값을 들여다보는 것이 핵심입니다. ‘35,000,000’ 안에 쉼표가 보이지요. 사람에게는 읽기 편한 구분 기호지만 int()에게는 숫자가 아닌 글자입니다. NameError, KeyError와 마찬가지로 “따옴표 안을 소리 내어 읽는” 독해법이 그대로 통합니다. TypeError와의 구분도 이 자리에서 정리해 두세요. 연산과 자료형의 조합 자체가 틀렸으면 TypeError, 조합은 옳은데 값이 규격 밖이면 ValueError입니다.

원인 후보.

  1. 쉼표, 통화 기호, 단위가 섞인 금액 문자열. "35,000,000", "3500원", " 3500" 같은 값들입니다. 실제 데이터를 복사해 오면 아주 흔히 겪습니다.
  2. 빈 문자열. input()에서 아무것도 안 치고 엔터만 누르면 int("")가 되어 같은 오류가 납니다.
  3. 소수점이 든 문자열을 int()로 바로 바꾸는 경우. int("3.5")는 ValueError입니다. float("3.5")로 먼저 실수로 바꾸는 우회로가 필요합니다.

수리는 변환 전에 숫자가 아닌 문자를 치우는 것입니다. 4장에서 배운 문자열 메서드 replace가 이 일에 알맞습니다.

income_text = "35,000,000"
income = int(income_text.replace(",", ""))    # 쉼표를 지운 뒤 변환
print(f"과세표준: {income:,}원")
과세표준: 35,000,000원

한 줄씩 뜯어보기

  • income_text.replace(",", "") — 문자열 메서드 replace는 첫 인자를 둘째 인자로 바꾼 새 문자열을 돌려줍니다. 쉼표를 빈 문자열로 바꾸었으니 결과는 "35000000", 쉼표만 사라진 문자열입니다.
  • int(...) — 쉼표가 사라진 문자열은 이제 십진수로 읽히므로 변환이 통과합니다. 괄호가 겹칠 때는 이렇게 안쪽에서 바깥쪽 순서로 읽으면 됩니다.
  • print(f"... {income:,}원") — 마지막에 f-string의 :, 서식이 쉼표를 도로 붙여 줍니다. 계산할 때는 쉼표를 벗기고, 보여줄 때는 쉼표를 입힌다. 값의 세계와 표시의 세계를 구분하는 이 감각은 2장에서 시작해 책 끝까지 쓰입니다.

13.4 에러가 나지 않을 때: 디버깅 습관 다섯 가지

사전이 도와줄 수 있는 것은 파이썬이 멈추고 보고서를 남긴 경우까지입니다. 그런데 더 까다로운 상대는 따로 있습니다. 에러 없이 끝까지 돌고 나서 틀린 답을 내놓는 코드입니다. 3장에서 조건의 순서를 바꿨더니 경기 국면이 조용히 엉뚱하게 분류되던 것을 기억할 겁니다. 보고서가 없으니 사전을 찾을 수도 없습니다. 이럴 때 필요한 것이 도구가 아니라 습관입니다. 다섯 가지를 소개합니다. 앞의 셋은 혼자 잡는 습관, 뒤의 둘은 잘 묻는 습관입니다.

습관 1. print로 중간값 찍기

프로그램이 도는 동안 변수 안의 값은 보이지 않습니다. 조용한 버그는 그 어둠 속에 숨어 있습니다. 가장 원시적이면서 가장 널리 쓰이는 조명이 print입니다. 결과가 의심스러우면, 최종 결과에 이르는 중간값들을 의심 지점마다 출력해 보는 것입니다.

연습 상대로, 3장의 누진세 계산기를 잘못 만든 코드를 하나 준비했습니다. 2024년 세율표의 앞 세 구간 — 1,400만 원 이하 6%, 5,000만 원 이하 15%, 8,800만 원 이하 24% — 을 옮긴다는 것이 어딘가 틀렸습니다. 일단 실행해 보겠습니다.

def income_tax(income):
    # 2024년 세율표의 앞 세 구간 (교육용 단순화)
    if income <= 14_000_000:
        tax = income * 0.06
    elif income <= 50_000_000:
        tax = income * 0.15
    else:
        tax = income * 0.24
    return tax

print(f"과세표준 1,400만 원의 세액: {income_tax(14_000_000):,.0f}원")
print(f"과세표준 1,500만 원의 세액: {income_tax(15_000_000):,.0f}원")
과세표준 1,400만 원의 세액: 840,000원
과세표준 1,500만 원의 세액: 2,250,000원

에러는 없습니다. 그런데 출력을 검산해 보면 이상합니다. 과세표준이 1,400만 원에서 1,500만 원으로 100만 원 늘었는데 세액은 84만 원에서 225만 원으로 141만 원이 늘었습니다. 소득이 는 것보다 세금이 더 늘어난 셈입니다. 어디서 많이 듣던 이야기지요. 3장에서 “구간이 바뀌면 오히려 손해”라는 흔한 오해를 숫자로 반박했는데, 그 오해가 지금 이 코드 안에서 실제로 벌어지고 있습니다. 실제 누진세는 그렇게 설계되어 있지 않으므로, 이것은 제도의 문제가 아니라 코드의 버그입니다.

노려보는 대신 print를 심습니다. 요령은 아무 데나 찍는 것이 아니라 검산 계획을 세우는 것입니다. 확인할 것은 두 가지입니다. 어느 분기로 들어갔는가, 그리고 세액이 어떤 식으로 계산되었는가.

def income_tax(income):
    if income <= 14_000_000:
        print("[확인] 1구간 진입, income =", income)
        tax = income * 0.06
    elif income <= 50_000_000:
        print("[확인] 2구간 진입, income =", income)
        print("[확인] 계산식: income * 0.15 =", income * 0.15)
        tax = income * 0.15
    else:
        print("[확인] 3구간 진입, income =", income)
        tax = income * 0.24
    return tax

print(f"결과: {income_tax(15_000_000):,.0f}원")
[확인] 2구간 진입, income = 15000000
[확인] 계산식: income * 0.15 = 2250000.0
결과: 2,250,000원

출력이 범인을 지목합니다. 1,500만 원이 2구간으로 들어간 것까지는 정상인데, 세액을 소득 전체에 15%를 곱해 계산했습니다. 3장에서 배운 대로 15%는 1,400만 원을 초과한 부분에만 붙어야 하고, 1,400만 원까지는 1구간 세액 84만 원이 그대로 얹혀야 합니다. 한계세율을 평균세율처럼 적용한, 경제학 개념의 오해가 그대로 코드가 된 버그입니다. 구간 하한까지의 세금에 초과분의 세금을 더하는 구조로 고치고, 경계값으로 검산합니다.

def income_tax(income):
    if income <= 14_000_000:
        tax = income * 0.06
    elif income <= 50_000_000:
        # 1,400만 원까지의 세액 84만 원 + 초과분의 15%
        tax = 840_000 + (income - 14_000_000) * 0.15
    else:
        # 5,000만 원까지의 세액 624만 원 + 초과분의 24%
        tax = 6_240_000 + (income - 50_000_000) * 0.24
    return tax

print(f"과세표준 1,400만 원의 세액: {income_tax(14_000_000):,.0f}원")
print(f"과세표준 1,500만 원의 세액: {income_tax(15_000_000):,.0f}원")
print(f"과세표준 5,000만 원의 세액: {income_tax(50_000_000):,.0f}원")
과세표준 1,400만 원의 세액: 840,000원
과세표준 1,500만 원의 세액: 990,000원
과세표준 5,000만 원의 세액: 6,240,000원

1,400만 원과 1,500만 원의 세액 차이가 15만 원, 즉 늘어난 100만 원에 한계세율 15%를 적용한 값으로 돌아왔습니다. 5,000만 원의 세액 624만 원은 3장의 누진공제 공식으로 구한 값과 일치합니다. 버그를 잡았으면 심어 두었던 확인용 print는 지우거나 주석 처리해서 출력을 원래대로 돌려놓는 것까지가 마무리입니다.

습관 2. 반씩 잘라 실행하기

스무 줄짜리 셀 어딘가에 버그가 있는데 위치를 모르겠다면, 스무 줄을 다 노려보는 대신 절반으로 자르세요. 앞 절반만 남기고(뒷부분은 잠시 주석 처리하거나 다른 셀로 옮기고) 실행해서 중간 결과가 옳은지 확인합니다. 옳다면 버그는 뒤 절반에, 틀리다면 앞 절반에 있습니다. 남은 절반을 또 반으로 자릅니다. 스무 줄이면 서너 번 만에 한두 줄로 좁혀집니다. 후보를 매번 절반씩 탈락시키는 탐색이라, 코드가 두 배로 길어져도 자르는 횟수는 한 번 늘어날 뿐입니다. 셀을 자유롭게 쪼갤 수 있는 노트북은 이 습관을 쓰기에 특히 좋은 환경입니다.

습관 3. 한 줄씩 소리 내어 설명하기

프로그래머들 사이에 고무오리 디버깅이라는 오랜 농담이 있습니다. 책상 위 고무오리 인형에게 코드를 한 줄씩 소리 내어 설명하다 보면 버그가 저절로 발견된다는 것입니다. 농담처럼 들리지만 원리는 진지합니다. “이 줄은 소득 전체에 15%를 곱하고… 아니, 잠깐. 전체에 곱하면 안 되는데?” 의도를 말로 옮기는 순간, 코드가 실제로 하는 일과 내가 하려던 일의 어긋남이 드러납니다. 눈으로 훑을 때는 하려던 일을 코드에 덧씌워 읽기 때문에 보이지 않던 차이입니다. 상대는 오리든 필통이든 상관없습니다. 동료에게 질문을 쓰다가 쓰는 도중에 스스로 답을 찾아 버리는 경험을 언젠가 하게 될 텐데, 그것이 바로 이 원리가 작동한 순간입니다.

습관 4. 에러 메시지로 검색하기

여러분이 만난 오류는 거의 확실히 다른 누군가가 먼저 만난 오류입니다. 검색만 제대로 하면 됩니다. 검색창에 넣을 것은 트레이스백의 마지막 줄입니다. 단, 내 상황에만 해당하는 부분은 지우고 넣으세요. KeyError: '카페 라떼'에서 ’카페 라떼’는 내 딕셔너리 사정이니, python KeyError dictionary처럼 일반화해야 남들의 사례와 만납니다. 반면 TypeError: can only concatenate str (not "int") to str는 문장 전체가 일반적이므로 통째로 넣는 편이 정확합니다. 메시지를 한국어로 번역해서 검색하지 마세요. 세계의 질문과 답은 영어 원문으로 쌓여 있습니다. 검색 결과에서는 같은 메시지를 인용한 질문을 찾고, 채택된 답이 무엇을 고치라고 하는지, 왜 그런지를 읽습니다. 고치라는 코드만 베끼고 이유를 건너뛰면 다음 주에 같은 검색을 반복하게 됩니다.

습관 5. AI에게 물을 때: 좋은 질문의 세 재료

요즘은 AI에게 오류를 물어보는 것이 자연스러운 시대이고, 이 책도 그 사용을 막지 않습니다. 다만 질문의 질이 답의 질을 정합니다. “코드가 안 돌아가요”라는 질문에는 AI도 조교도 점쟁이 노릇밖에 할 수 없습니다. 좋은 질문에는 세 가지 재료가 들어갑니다. 첫째, 코드 전문. 문제의 셀만이 아니라 그 셀이 의존하는 위쪽 셀까지입니다. 둘째, 에러 메시지 전문. 마지막 줄만 오려 내지 말고 트레이스백을 통째로 붙입니다. 여러분이 이 부록에서 배운 바로 그 정보들(사건 현장, 호출 경로)을 AI도 그 안에서 읽습니다. 셋째, 시도한 것과 기대한 결과. 이 재료가 있어야 답이 여러분의 수준과 맥락에 맞춰집니다.

[상황] 3장의 누진세 계산기를 만드는 중입니다. 과세표준 5,000만 원을 넣으면
세액 대신 오류가 납니다.

[코드]
(셀 전체를 그대로 붙여넣기)

[에러 메시지]
(트레이스백 전체를 그대로 붙여넣기)

[시도한 것] 콜론은 확인했고, income을 print해 보니 50000000이 맞게 나옵니다.
[기대한 결과] 산출세액 6,240,000원이 나와야 한다고 생각합니다.

답을 받은 뒤가 중요합니다. 고쳐 준 코드를 붙여 넣고 끝내지 말고, 무엇이 왜 바뀌었는지 자기 말로 설명할 수 있는지 확인하세요. 설명이 안 되면 그 부분을 다시 물으면 됩니다. AI의 답이 늘 옳은 것도 아니어서, 설명을 요구하는 습관은 잘못된 답을 거르는 안전망이기도 합니다. 디버깅의 목적은 오늘 이 오류를 없애는 것이 아니라, 다음에 같은 오류를 스스로 잡는 것입니다.

실수 노트 — “안 돼요”라는 질문과 스크린샷 첨부

도움을 청하는 메시지의 최악의 형태는 “안 돼요”라는 말과 화면을 폰 카메라로 찍은 사진의 조합입니다. 사진 속 코드는 받는 사람이 실행해 볼 수도, 트레이스백을 복사해 검색할 수도 없습니다. 코드와 에러 메시지는 반드시 텍스트로 복사하세요. 셀의 코드는 셀 안에서 전체 선택해 복사하고, 트레이스백도 출력 영역을 드래그하면 글자로 복사됩니다. 질문을 받는 상대가 조교든 친구든 AI든, 상대가 내 상황을 재현할 수 있는 형태로 건네는 것이 예의이자 가장 빨리 답을 받는 길입니다.

13.5 연습문제: 버그 찾기

이 부록의 연습문제는 새 코드를 짜는 것이 아니라 남이 짠 코드의 버그를 잡는 것입니다. 다섯 문제 모두 진행 방법이 같습니다. 코드를 아래 작업 공간에 붙여 넣고 실행해 보세요. 에러가 나면 트레이스백을 마지막 줄부터 읽고, 에러가 나지 않으면 출력이 옳은지 검산하세요. 답안은 두 부분입니다. (1) 버그가 무엇인지 한 문장으로 진단하고, (2) 고친 코드를 제시하세요.

문제 B-1. 경계에 선 납세자 — 소득세의 2구간은 “1,400만 원 초과 5,000만 원 이하”이므로, 과세표준이 정확히 5,000만 원인 사람의 한계세율은 15%여야 합니다. 그런데 아래 코드는 24%라고 답합니다. 에러는 나지 않습니다. 버그를 진단해 고치고, 1,400만 원·5,000만 원·8,800만 원의 세 경계값을 넣어 검산하세요.

income = 50_000_000

if income <= 14_000_000:
    bracket = "1구간, 한계세율 6%"
elif income < 50_000_000:
    bracket = "2구간, 한계세율 15%"
elif income <= 88_000_000:
    bracket = "3구간, 한계세율 24%"
else:
    bracket = "4구간 이상"

print(bracket)

문제 B-2. 사라진 1월 — 이콘 카페의 12개월 판매량을 합산하는 코드입니다. 실행하면 에러가 납니다. 트레이스백의 마지막 줄을 읽고 원인을 진단한 뒤, 두 가지 서로 다른 방법으로 고치세요. 하나는 range의 인자 수정, 하나는 len의 활용입니다. 고친 코드의 연간 합계가 4,016잔이면 정답입니다. 덧붙여 생각해 볼 것 하나. 에러만 없어지고 합계는 틀리게 만드는 수리법도 있습니다. 어떤 수리법이 그런 함정이 되는지, 왜 그런지도 설명해 보세요.

monthly_sales = [310, 285, 342, 360, 355, 298, 275, 330, 361, 372, 340, 388]

total = 0
for month in range(1, 13):        # 1월부터 12월까지라는 생각
    total = total + monthly_sales[month]

print(f"연간 판매량: {total}잔")

문제 B-3. 151515…원짜리 주급 — 편의점 알바의 주급을 계산하는 코드입니다. 15를 입력했더니 에러 없이 실행됐는데, 출력이 주급이 아니라 15가 10,320번 이어 붙은 괴물 문자열이었습니다. 왜 TypeError조차 나지 않았는지 사전의 TypeError 항목을 근거로 설명하고, 코드를 고치세요. 15시간 기준 주급이 154,800원으로 나오면 정답입니다.

hours_text = input("이번 주 일한 시간: ")   # 15 를 입력했다고 하자
wage = 10_320                               # 2026년 최저시급(원)
pay = hours_text * wage

print(f"이번 주 주급: {pay}원")

문제 B-4. 두 장부의 어긋남 — 한 달 생활비의 일부를 품목별 수량과 단가로 계산하는 코드입니다. 실행하면 에러가 납니다. 트레이스백 마지막 줄에 인용된 것을 단서로 두 딕셔너리를 대조해 버그를 찾고, 고친 뒤 합계가 251,000원인지 확인하세요.

quantities = {"학식": 22, "아메리카노": 30, "교통카드 충전": 4}
prices = {"학식": 6000, "아메리카노": 2300, "교통 카드 충전": 12500}

total = 0
for item in quantities:
    total = total + quantities[item] * prices[item]

print(f"한 달 생활비(일부): {total:,}원")

문제 B-5. 배달비 할인이 적용되지 않는다 — 주문 금액이 2만 원 이상이면 배달비 1,000원, 1만 원 이상이면 3,000원, 그 미만이면 3,500원인 요금표를 코드로 옮겼습니다. 그런데 24,000원어치를 주문해도 배달비가 3,000원으로 나옵니다. 에러는 나지 않습니다. 두 번째 조건이 왜 영영 실행될 기회를 얻지 못하는지 설명하고, 두 가지 방법으로 각각 고치세요. 하나는 조건의 순서 바꾸기, 하나는 조건식에 상한 추가하기입니다.

order = 24_000

if order >= 10_000:
    fee = 3_000
elif order >= 20_000:
    fee = 1_000
else:
    fee = 3_500

print(f"배달비: {fee:,}원")

다섯 문제의 버그 진단과 고친 코드는 부록 C에 있습니다.

# [작업 공간] 버그 찾기
# TODO: 문제의 코드를 이 셀에 붙여 넣고 실행하며 버그를 잡아 보세요.
# TODO: 에러가 나면 트레이스백을 마지막 줄부터 읽고,
#       에러가 나지 않으면 출력을 손으로 검산하세요.

13.6 요약: 빨간 글자와 친해지기

에러 메시지는 꾸중이 아니라 파이썬이 남기는 사고 보고서이고, 읽는 순서는 정해져 있습니다. 마지막 줄에서 무엇이 왜 잘못됐는지를 확인하고, 화살표가 가리키는 줄에서 사건 현장을 확인하고, 필요하면 호출 경로를 따라 위로 올라갑니다. 이 책의 실습에서 만나는 오류는 대부분 여덟 종류 안에 있으며, 그 메시지들에는 문제의 이름·키·값이 따옴표 안에 그대로 인용되어 있으므로 인용된 부분을 소리 내어 읽는 것이 진단의 절반입니다. 에러 없이 틀린 답을 내는 조용한 버그는 print로 중간값을 비추고, 반씩 잘라 범위를 좁히고, 한 줄씩 남에게 설명하는 습관으로 잡습니다. 도움을 청할 때는 코드 전문과 트레이스백 전문, 그리고 시도한 것을 함께 건네야 상대가 사람이든 AI든 제대로 도울 수 있습니다. 결국 디버깅은 타고나는 감이 아니라 따라 하면 되는 절차라는 것, 그것이 이 부록에서 가져갈 한 문장입니다. 본문을 공부하다 빨간 글자를 만나면 언제든 이 사전으로 돌아오세요. 이 부록의 다섯 문제를 포함해 책의 모든 연습문제 풀이는 부록 C에 모여 있습니다.