Python
Step 46. Exception Handling and Debugging — Programs That Get Back Up When They Fall
Level 1 — Programming and the Computer’s Inner Workings | Difficulty ★★☆☆☆ | Estimated time: 3 hours
Prerequisites: Steps 41–45 complete. You can use functions, file I/O, and data structures.
- What you need: a computer with Python installed and a text editor. Nothing new to install.
- Caution: today’s exercise is 100% safe. There are many experiments that deliberately cause errors, but they all happen only inside the small scripts we create.
The programs we’ve made so far share one thing: they only deal with well-behaved users. Asked for a number, they get a number; files get opened only when they exist. But real users write "twenty" when asked their age, misspell file names, and hit Enter on an empty field. If the program spews red text and dies every time, it’s not a tool — it’s a glass figurine. Today we learn breakfalls for programs — not programs that never fall, but programs that get back up when they fall.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Read an error message (traceback) from the bottom up to find the cause and the scene
- Distinguish the representative exceptions by name (ZeroDivisionError, ValueError, FileNotFoundError)
- Build per-exception-type responses with
try/exceptso the program doesn’t die - Build a "if wrong, ask again" input loop with the
while + trycombination - Make a function reject bad values on its own with
raise
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Python 3 (running script files) |
| Today’s grammar | try / except, else, finally, raise, except ... as e |
| Exceptions you’ll meet today | ZeroDivisionError, ValueError, FileNotFoundError |
| Concepts needed | exceptions, tracebacks, defensive programming |
2-1. Exceptions — The "I Can’t Go Any Further" Signal
When Python meets a situation it can’t handle during execution, it throws an exception and stops. Dividing by zero, converting text to a number, and opening a nonexistent file are the classics.
An exception is not Python getting angry — it’s an accurate situation report. Where, what, and why it failed are all written down. That’s why experts read error messages not as "failure" but as "the most honest documentation."
2-2. Tracebacks — The Incident Report
When an exception occurs, you get output like this (verified on 2026-09-09, file path shortened):
Traceback (most recent call last):
File "e1_zero.py", line 1, in <module>
print(1 / 0)
~~^~~
ZeroDivisionError: division by zero
This is called a traceback. The reading trick is bottom to top.
- The bottom line:
ZeroDivisionError: division by zero— what the accident was (division by zero) - The line above:
File "e1_zero.py", line 1— where it happened (line 1) - Caret marks like
~~^~~: from Python 3.11 on, the position of the expression at fault is underlined for you
No matter how long it gets, the principle is the same: read from the bottom. This one ordering greatly speeds up debugging.
2-3. try/except — Setting Up the Safety Net
The syntax for catching exceptions is try/except.
try:
x = int(input("Number: "))
print(10 / x)
except ZeroDivisionError:
print("You can't divide by zero.")
tryblock: where you put work that might be dangerousexceptblock: the countermeasure to run instead if that accident happens
Even when an exception occurs, the program doesn’t die — it runs the except countermeasure and keeps going. It’s falling and rolling with a breakfall, then standing back up.
2-4. Defensive Programming — Don’t Assume Users Are Fairies
The philosophy of exception handling has a name: defensive programming. An attitude that takes "input can always be weird" as the default assumption.
Ask for a number but prepare for text to arrive; open a file but prepare for it to be missing. With this attitude, programs grow solid — and this attitude itself is also the root of security thinking.
3. Follow Along
3-1. Kill It on Purpose — Meet the Three Exception Brothers
Input (e1_zero.py)
print(1 / 0)
Run it and the traceback from earlier appears. Now change the file.
Input (e2_value.py)
print(int("abc"))
Traceback (most recent call last):
File "e2_value.py", line 1, in <module>
print(int("abc"))
^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'abc'
(Verified on 2026-09-09, path shortened.)
Next.
Input (e3_file.py)
open("missing_file.txt")
Traceback (most recent call last):
File "e3_file.py", line 1, in <module>
open("missing_file.txt")
FileNotFoundError: [Errno 2] No such file or directory: 'missing_file.txt'
(Verified on 2026-09-09, path shortened.)
How to read it: write the three exception names in your notebook. The name is the diagnosis itself — ZeroDivisionError (divided by zero), ValueError (unsuitable value), FileNotFoundError (file not found). They’re friends you’ll meet hundreds more times. And notice: once an exception occurs, the code below it never runs. The program stops right there.
Predict: what exception will
int("3.14")raise? "3.14" is a decimal, but int wants an integer. Predict, then check. (Verified result on 2026-09-09:ValueError: invalid literal for int() with base 10: '3.14'— even a decimal-point string gets refused integer conversion.)
3-2. First Safety Net — Division That Won’t Die
Input (safe1.py)
try:
x = int(input("Enter a number: "))
print(f"10 divided by {x} is {10 / x}")
except ValueError:
print("That's not a number! Please enter digits.")
except ZeroDivisionError:
print("You can't divide by zero.")
print("The program ended normally.")
Test with three kinds of input. The case of entering 0 (verified on 2026-09-09):
Enter a number: You can't divide by zero.
The program ended normally.
The case of entering text:
Enter a number: That's not a number! Please enter digits.
The program ended normally.
The case of valid input (5):
Enter a number: 10 divided by 5 is 2.0
The program ended normally.
(Verified on 2026-09-09. The entered value prints right after the prompt.)
How to read it: attach two excepts and each accident type gets a different response. ValueError (text input) and ZeroDivisionError (zero input) each have their own line. And in every case, the last line runs — proof the program didn’t die.
Why: different accidents need different responses so the user knows the next action. "Please retype" and "zero isn’t allowed" are different guidance.
3-3. Close Reading of a Traceback
Let’s deliberately make a slightly longer incident.
Input (trace.py)
def divide(a, b):
return a / b
def start():
x = 10
y = 0
print(divide(x, y))
start()
Output (verified on 2026-09-09, path and caret marks shortened):
Traceback (most recent call last):
File "trace.py", line 10, in <module>
start()
File "trace.py", line 8, in start
print(divide(x, y))
File "trace.py", line 3, in divide
return a / b
ZeroDivisionError: division by zero
How to read it: read bottom to top. The diagnosis is division by zero; the accident scene is line 3 (inside divide); the path that led there is stacked as line 8 → line 10. The deeper functions go, the longer the trail of footprints, but the reading method is identical. Diagnosis from the last line; the real scene from the bottom-most File line.
Why: now that you’ve learned functions, most errors will come as "multi-layered trails" like this. Today you learn how not to be intimidated.
3-4. Asking Again — The Appeal Loop
Combine except with while and you get a patient program that "asks again when wrong."
Input (patient.py)
while True:
try:
age = int(input("Enter your age as a number: "))
break
except ValueError:
print("That's not a number. Please try again.")
print(f"You're {age}. Thank you!")
A run where "abc", "fifteen", and an empty field (just Enter) were entered in turn, then finally 15 (verified 2026-09-09):
Enter your age as a number: That's not a number. Please try again.
Enter your age as a number: That's not a number. Please try again.
Enter your age as a number: That's not a number. Please try again.
Enter your age as a number: You're 15. Thank you!
How to read it: every wrong input makes except say its line and while loops back to the start. An empty field also gets the same line because int("") raises ValueError. When a valid input arrives, try succeeds and break escapes. "Ask again until it succeeds" is the national-standard pattern of user input handling.
3-5. The File Safety Net — Giving the Notepad a Breakfall
Let’s give Step 45’s notepad a breakfall.
Input (safememo.py)
try:
with open("diary.txt", "r", encoding="utf-8") as f:
print(f.read())
except FileNotFoundError:
print("No diary yet. Start with your first memo!")
Run with no diary.txt present (verified on 2026-09-09):
No diary yet. Start with your first memo!
How to read it: in Step 45 we asked in advance with os.path.exists. Today’s style is "just try it, and handle the accident if it happens." Both styles are widely used, and the Python world tends to prefer the try style. Know both.
3-6. finally and else — Work to Do Regardless of Accidents
Input (elsefinally.py)
try:
x = int(input("Number: "))
except ValueError:
print("That's not a number.")
else:
print("Input succeeded! Starting calculation.")
finally:
print("--- Input processing done ---")
The case of entering 7 and the case of entering abc (verified on 2026-09-09):
Input succeeded! Starting calculation.
--- Input processing done ---
That's not a number.
--- Input processing done ---
How to read it: else runs "only when there was no accident"; finally runs "unconditionally, accident or not." That both runs show --- Input processing done --- at the end is the proof of finally. finally’s representative use is finishing work that must happen in any case — "closing files, cleanup."
3-7. raise — We Can Throw Exceptions Too
Python isn’t the only one that can throw exceptions. Our code can throw one too when it judges "this isn’t right."
Input (raisetest.py)
def set_age(age):
if age < 0 or age > 150:
raise ValueError("Age must be between 0 and 150.")
return age
try:
set_age(-5)
except ValueError as e:
print("Rejected:", e)
Output (verified on 2026-09-09):
Rejected: Age must be between 0 and 150.
How to read it: raise ExceptionName("message") is "accident declared here!" The e in except ... as e holds that message. This pattern — "a function rejects values it must not accept" — is a device by which the function’s maker protects its users.
Why: later, when you build tools, loudly rejecting misuse is far safer than quietly letting it pass. A quiet misdiagnosis is the most dangerous thing.
4. Missions & Exercises
Mission — The Invincible Calculator
Build the invincible calculator based on Step 44’s menu-driven calculator. Requirements:
- Everywhere a number is asked, it doesn’t die on text, empty fields, or weird symbols — it asks again
- Entering 0 in division doesn’t kill it — it prints "You can’t divide by zero"
- An out-of-range number or text for the menu choice gets guidance: "Enter 1-4 or 0"
- It absolutely never dies until the user picks 0 (exit)
How to test: feed it ten or more bad inputs yourself. If possible, ask family or a friend to "try to break it." Defense is completed when tested aggressively.
Exercises
Question 1. A long traceback was printed. Which line should you read first, and what does each line tell you?
Question 2. What exception does running int("3.14") raise, and why?
Question 3. If you write except: with no exception name, what problem arises?
Question 4. Explain why putting dozens of lines of code in a try block is a bad habit.
Answers & completion criteria · expand/collapse
5. Model Answers & Completion Criteria
Mission Model Answer
An example of the core structure (the full code is Step 44’s calculator with the pattern below applied):
def ask_number(prompt):
while True:
try:
return int(input(prompt))
except ValueError:
print("Please enter a number.")
while True:
print("1.Add 2.Subtract 3.Multiply 4.Divide 0.Exit")
menu = input("Choice: ").strip()
if menu == "0":
break
if menu not in ["1", "2", "3", "4"]:
print("Enter 1-4 or 0.")
continue
a = ask_number("First number: ")
b = ask_number("Second number: ")
if menu == "1":
print("Result:", a + b)
elif menu == "2":
print("Result:", a - b)
elif menu == "3":
print("Result:", a * b)
else:
try:
print("Result:", a / b)
except ZeroDivisionError:
print("You can't divide by zero.")
How to verify: ① Does it ask again when "abc", an empty field, or "3.14" goes into a number slot? ② Does entering 0 in division show the guidance and return to the menu? ③ Does entering "9" or "menu" at the menu show guidance? ④ After passing through all of this, does it still not end before you press 0? Four ‘yes’es and you pass.
Design point: extracting "ask for a number" into a function (ask_number) lets you write the while + try loop once and reuse it. Examples of ten bad-input tests: text, empty field, decimal, negative, a huge number, spaces only, special characters, spelled-out numbers ("three"), a tab character, 0.5 at the menu.
Exercise Answers
Answer 1. Read from the bottom line up. The bottom line is the diagnosis (exception name and reason), the File line above it is the accident scene (file name and line number), and the File lines further up are the call path that led there. The order is "diagnosis → scene → path."
Answer 2. It raises ValueError (verified on 2026-09-09: ValueError: invalid literal for int() with base 10: '3.14'). Because "3.14" is a string representing a decimal, it doesn’t fit the integer conversion rule. What int() accepts is only integer-shaped strings like "10" or "-3".
Answer 3. A bare except means "swallow every accident," so it quietly hides not only the accidents you expected but also real bugs like typos. You can confirm this by experiment — deliberately mistyping pritn and wrapping it in a bare except made it pass silently with no error report (verified on 2026-09-09). A quiet bug is the hardest bug to find. The principle is to write a specific name, like except ValueError:.
Answer 4. With a wide try, you catch the exception but can’t pinpoint which line caused it. The iron rule is to set the safety net only over the one or two dangerous lines; if dangerous lines are in several places, split the try into several.
Completion Criteria Checklist
- [ ] I can read a traceback from the bottom up and find the diagnosis and the scene
- [ ] I can distinguish ZeroDivisionError, ValueError, and FileNotFoundError
- [ ] I can build per-exception-type responses with try/except
- [ ] I can build "ask again when wrong" with while + try
- [ ] I can explain the execution conditions of else and finally
- [ ] I can make a function reject bad values with raise
- [ ] Mission: I completed the invincible calculator and tested it with ten bad inputs
6. Common Pitfalls & Fixes
Wall 1. It dies even though I added an except
Symptom: you used try/except but red text still appears.
Cause: two representative cases. (a) The line that raised the exception is outside the try block. (b) The exception type you catch is different — a ValueError-catching except, but a FileNotFoundError occurred.
Fix: check whether the exception name at the bottom of the traceback matches the name on your except, and whether the erroring line is inside the try. The safety net must be set only above the performance area.
Wall 2. The try is too wide
Symptom: it catches the exception, but you can’t tell where it came from.
Cause: you put dozens of lines of code in the try block. With a wide net, the accident scene can’t be pinpointed.
Fix: put only "the truly dangerous one or two lines" in a try. If dangerous lines are in several places, split the try into several.
Wall 3. Slapping on a bare except (except:)
Symptom: you wrote except: with no exception type, and even a typo bug got swallowed, so you searched for the cause for ages.
Cause: a bare except swallows every exception — even the NameError from a syntax slip. In the 2026-09-09 experiment, wrapping pritn("typo") in a bare except let it pass silently with no report.
Fix: write exception names concretely, like except ValueError:. The act of writing "what accident am I expecting" is design in itself.
Wall 4. Reading the error message from the top
Symptom: when a long traceback appears, you read from the first line and burn out.
Cause: the order is reversed. The first line merely "announces that an incident occurred"; the diagnosis is at the very bottom.
Fix: always start from the bottom line. Get the order into your hands: "diagnosis (bottom) → scene (the File line above) → path (further up)."
Wall 5. Empty input behaves unexpectedly
Symptom: you just hit Enter and instead of "not a number," it dies or a weird value goes in.
Cause: input() returns the empty string "" for empty input, and int("") raises ValueError. If an except exists, asking again is normal behavior; if it died, you converted outside the try.
Fix: use 3-4’s loop structure as-is. An empty field gets filtered naturally as one kind of bad input (confirmed by testing on 2026-09-09).
7. Summary
Today’s Concepts
| Concept | One-line description |
|---|---|
| Exception | Python’s accurate situation report — when raised, it stops right there |
| Traceback | The incident report. From the bottom: diagnosis → scene → path |
| Defensive programming | Taking "input can always be weird" as the default assumption |
| ZeroDivisionError | Division by zero |
| ValueError | A value doesn’t fit the rule (text→number conversion failure, etc.) |
| FileNotFoundError | Tried to open a nonexistent file |
Today’s Grammar
| Grammar | What it does |
|---|---|
try: |
Starts the block holding dangerous work |
except ExceptionName: |
The countermeasure to run if that accident happens |
except ... as e: |
Receive the exception message as e |
else: |
Runs only when there was no accident |
finally: |
Runs unconditionally, accident or not |
raise ExceptionName("message") |
Our code declares an accident |
while True + try + break |
The "ask again when wrong" national pattern |
Instincts More Important Than Commands
When an error pops up, a beginner closes the window, an intermediate reads the message, and an expert reproduces it. "On what input did it die? Let’s feed the same input again." Reproduce → diagnose (bottom line) → hypothesize → confirm with print → fix. Make these five beats a habit starting today. And once fixed, always retest with the same input — a retest is the only thing that proves a "fix."
Let me add one security perspective. A traceback is friendly documentation for the developer, but shown raw to users, it amounts to disclosing internal structure like file paths and function names. That’s why real services do dual processing: "record in detail internally, but show the user only ‘a temporary error occurred.’" For now your work is lab practice, so it doesn’t matter — but remember this principle.
Today your program learned breakfalls, and you yourself learned to read red text not as fear but as documentation. Next time you meet red text, read from the bottom, calmly, as if listening to an assistant’s report. The answer will always be visible.
Once every box is checked, Step 46 is complete.
ONE STEP FURTHER
Finished this lesson?
Check the completion criteria, then mark your progress.