Lesson 14: One Bad Line Can Crash Your Whole Program
š Full written solution: https://interview-kit-fe.vercel.app/python-lesson-14-error-handling-with-exceptions š» Code on GitHub: https://github.com/timepasshub2539-hue/leetcode-python/tree/main/python-lesson-14-error-handling-with-exceptions Chapters: 0:00 One Bad Line Can Crash Your Whole Program 0:17 The Vocabulary First 0:36 Everyday Analogy 0:53 Basic Try and Except 1:09 Catching Multiple Error Types 1:25 The Else Clause 1:41 The Finally Clause 1:59 Full Flow Together 2:16 Raising Your Own Exceptions 2:32 Match the Error to the Crime 2:54 Common Mistake: Bare Except 3:13 Common Mistake: Catching Too Broadly 3:35 Try/Except vs Checking First 3:59 Recap Learn how one bad line can crash an entire Python program, and how try/except/else/finally stops it cold. This lesson covers catching multiple error types, raising your own exceptions, matching exception types to the right bug, and why bare except and overly broad catches quietly break your code. By the end you'll know exactly when to catch an error versus check first. #python #exceptionhandling #tryexcept #pythontutorial #codingforbeginners Watch next: - Stop Over-Catching Errors: The Try/Except Truth in Lesson 14: https://youtu.be/8SmJ2EnV8so - Sorting with ORDER BY #shorts: https://youtu.be/OpETVzevDok - Python Lesson 13: Turn 4 Lines Into 1 With Comprehensions: https://youtu.be/-AV9Nakus-U
Introduction
Every developer eventually ships code that meets input it wasn't expecting. A user uploads a file that doesn't exist. A config value that should be a number arrives as a string. A network call that usually succeeds just... doesn't, this one time. None of these are exotic scenarios ā they're Tuesday. The question isn't whether your program will encounter bad input, it's whether it has a plan for when it does.
This matters far beyond personal projects. In coding interviews, exception handling is one of the fastest ways to separate a candidate who writes code that "works on the happy path" from one who writes code that survives contact with reality. Interviewers routinely probe this with questions like "what happens if this list is empty?" or "how would you handle a missing key here?" If your answer is a shrug, that's a signal. If your answer is "I'd catch that specific exception and handle it explicitly," that's a very different signal.
Learning try/except/else/finally well is also one of those rare topics in Python where understanding it deeply pays off immediately ā it changes how you read tracebacks, how you structure functions that touch files or networks, and how you reason about what "correct" even means when things go wrong.
Problem Overview
Here's the situation in plain terms: Python code executes top to bottom, line by line. If any line raises an exception ā an error condition Python detects at runtime ā and nothing is there to catch it, the program stops immediately. Every line after the failure point never runs. The user sees a traceback: a wall of red text that means nothing to someone who isn't debugging the code themselves.
The goal is to build a mechanism where:
- Risky code (file access, type conversion, division, dictionary lookups) is clearly marked as risky.
- When something goes wrong inside that risky code, you catch the specific problem instead of letting the whole program die.
- Code that should only run after success has a dedicated place to live.
- Cleanup code ā closing files, releasing connections ā runs no matter what happened, success or failure.
Python solves this with four keywords: try, except, else, and finally. You don't need all four in every block, but understanding what each one guarantees is what lets you reason correctly about your program's behavior under failure.
Example
Consider a small function that reads a user's age from input and validates it:
def get_age(raw_value):
age = int(raw_value)
if age < 0:
raise ValueError("age cannot be negative")
return age
print(get_age("25")) # works fine -> 25
print(get_age("hello")) # crashes the whole program
print(get_age("-3")) # never even runs
Run this, and the moment get_age("hello") executes, int("hello") raises a ValueError. Python has no instructions for what to do next, so it prints a traceback and exits. The third call, get_age("-3"), never gets a chance to run ā not because it's wrong, but because the program already died one line earlier.
Now compare that to a version with exception handling:
def get_age(raw_value):
try:
age = int(raw_value)
except ValueError:
print(f"'{raw_value}' is not a valid number")
return None
else:
if age < 0:
print("age cannot be negative")
return None
return age
for value in ["25", "hello", "-3"]:
print(get_age(value))
Output:
25
'hello' is not a valid number
None
age cannot be negative
None
Every input gets processed. The bad ones produce a clear message instead of a crash, and the loop keeps going. That's the entire point.
Intuition
Here's how an experienced engineer actually thinks about this, before writing a single line of try/except.
Picture a tightrope walker. The walk itself ā the risky part ā is the try block. It might go fine. It might not. If the walker falls, there's a safety net underneath: that's the except block, waiting specifically for a fall, not for anything else that could happen in the building. If the walk succeeds without incident, the crowd cheers ā that's else, code that should only run when nothing went wrong. And no matter what happened, fall or no fall, the crew rolls up the mats afterward. That's finally. It always runs.
The key intuition experienced engineers rely on: an exception is not a bug report, it's a named event. ValueError, TypeError, KeyError, IndexError ā each name tells you exactly what kind of thing went wrong. That naming is the whole reason exception handling is more powerful than a generic "something failed" flag. When you catch ValueError specifically, you're saying "I anticipated exactly this failure mode, and I know what to do about it." When you catch everything with a bare except, you're saying "I have no idea what might go wrong, so I'll hide all of it" ā which also means hiding your own typos and logic errors alongside the failure you actually expected.
The second intuition: Python culture favors asking forgiveness rather than permission (EAFP) over checking first (LBYL). You could check if key in dictionary before accessing it, or you could just try the access and catch KeyError if it's missing. For file and network operations especially, checking first doesn't even guarantee success a moment later ā the file could be deleted, the connection could drop, between your check and your action. Trying the operation and catching the specific failure is usually the more honest, more reliable pattern.
Brute Force Solution
The "brute force" approach here is really the absence of exception handling, paired with manual pre-checks for every possible failure mode.
Idea: Before doing anything risky, check every condition that could possibly cause a problem.
def get_age_checked(raw_value):
if not raw_value.lstrip("-").isdigit():
print(f"'{raw_value}' is not a valid number")
return None
age = int(raw_value)
if age < 0:
print("age cannot be negative")
return None
return age
Advantages:
- No exceptions involved, easy to trace line by line for simple cases.
- Works fine for pure, self-contained validation like this.
Disadvantages:
- Doesn't scale. Every new failure mode needs a new manual check, and it's easy to miss one.
- Breaks down completely for anything involving timing, like files or networks ā the state can change between your check and your action (a file can be deleted right after you confirm it exists).
- Duplicates logic that Python's own parsing already knows how to detect (writing your own "is this a valid integer" check is reinventing what
int()already does internally).
Complexity: Time O(n) for the string scan in this example, O(1) space. But complexity isn't really the point here ā correctness under real-world timing is.
Optimal Solution
The optimal approach is to let Python attempt the risky operation and catch the specific exception it raises, rather than pre-validating everything yourself.
The mental model is a pipeline:
| Stage | Runs when | Purpose |
|---|---|---|
try |
Always attempted first | Contains the risky code |
except SpecificError |
Only if that exact exception was raised | Handles one named failure |
else |
Only if try succeeded with zero exceptions |
Code that depends on success |
finally |
Always, regardless of outcome | Guaranteed cleanup |
Python checks except clauses top to bottom and runs the first one that matches the raised exception type. If none match, the exception keeps propagating up the call stack ā it does not get silently ignored.
try:
result = 10 / 0
except ZeroDivisionError:
print("cannot divide by zero")
except ValueError:
print("this won't fire, wrong exception type")
Here, only the ZeroDivisionError handler runs, because that's the exception Python actually raised. The order of the except clauses matters only in that Python stops at the first match ā so put more specific exceptions before more general ones if there's any overlap.
Python Code
class InvalidAgeError(ValueError):
"""Raised when an age value fails business-rule validation."""
def parse_age(raw_value: str) -> int:
"""
Convert a raw string into a validated age.
Raises InvalidAgeError if the value isn't a valid non-negative integer.
"""
try:
age = int(raw_value)
except ValueError as exc:
raise InvalidAgeError(f"'{raw_value}' is not a valid number") from exc
else:
if age < 0:
raise InvalidAgeError("age cannot be negative")
return age
def process_ages(raw_values: list[str]) -> list[int]:
valid_ages = []
for raw_value in raw_values:
try:
age = parse_age(raw_value)
except InvalidAgeError as exc:
print(f"Skipping invalid entry: {exc}")
continue
else:
valid_ages.append(age)
finally:
print(f"Finished processing '{raw_value}'")
return valid_ages
if __name__ == "__main__":
inputs = ["25", "hello", "-3", "42"]
print(process_ages(inputs))
Code Walkthrough
class InvalidAgeError(ValueError)ā a custom exception that inherits fromValueError. This means it can be caught either asInvalidAgeErrorspecifically, or asValueErrormore broadly, giving callers flexibility.try: age = int(raw_value)ā the only genuinely risky line.int()raisesValueErrorif the string can't be converted.except ValueError as exc: raise InvalidAgeError(...) from excā catches the specific conversion failure and re-raises it as our own domain-specific exception. Thefrom excpreserves the original traceback for debugging, rather than hiding where the error actually came from.else: if age < 0: raise InvalidAgeError(...)ā this only runs if the conversion succeeded. Separating "did the conversion work" from "is the value valid" keeps each check honest about what it's testing.- In
process_ages,except InvalidAgeError as exccatches only the exception type we anticipated ā a genuine bug elsewhere in the loop body would still surface normally instead of being swallowed. finally: print(...)ā runs for every single entry, whether it was valid, invalid, or even if something unexpected slipped through. This is the natural place for logging or cleanup that must happen no matter what.
Dry Run
Trace inputs = ["25", "hello", "-3", "42"] through process_ages:
"25"āparse_ageconverts to25,elsechecks25 < 0is false, returns25.valid_ages = [25].finallyprints."hello"āint("hello")raisesValueErrorā caught, re-raised asInvalidAgeError("'hello' is not a valid number"). Caught inprocess_ages, prints "Skipping invalid entry: ...".finallystill prints."-3"ā converts fine to-3,elsebranch catchesage < 0, raisesInvalidAgeError("age cannot be negative"). Caught, skipped,finallyprints."42"ā converts fine,42 < 0is false, returns42.valid_ages = [25, 42].finallyprints.
Final output: [25, 42], with four "Finished processing" lines interleaved with the skip messages ā proof that finally really does run on every iteration, success or failure.
Complexity Analysis
- Time: O(n) where n is the number of entries in
raw_valuesā each entry is processed exactly once, and exception handling itself adds no meaningful per-call overhead in the success path (Python's exception machinery is essentially free when no exception is raised; the cost is only paid when one actually fires). - Space: O(k) where k is the number of valid entries, for the
valid_ageslist being built. No auxiliary data structures scale with input size beyond that. - These bounds are correct because exception handling changes control flow, not the underlying algorithmic work ā you're still doing one pass over the input either way.
Alternative Solutions
LBYL (Look Before You Leap): Check raw_value.lstrip("-").isdigit() before calling int(). This works for pure computation like string parsing where there's no risk of state changing between the check and the action. It's a reasonable style choice here specifically because there's no external resource involved.
Where LBYL breaks down: anything involving files, network calls, or shared state. Checking os.path.exists(path) and then opening the file has a race condition ā the file can vanish in between. The EAFP style (try: open(path) and catching FileNotFoundError) is strictly more correct in these cases, because it doesn't assume the world stays still between your check and your action.
Edge Cases
- Empty input:
parse_age("")āint("")raisesValueError, correctly caught and converted toInvalidAgeError. - Whitespace-only input:
int(" ")also raisesValueError, handled the same way. - Exact boundary value:
parse_age("0")ā zero is a valid, non-negative age, so it should return0, not be rejected. Worth a dedicated test since0is falsy in Python and easy to accidentally mishandle with a carelessif age:check instead ofif age < 0:. - Very large numbers:
parse_age("99999999999999")ā Python integers have no fixed size limit, so this converts fine; any upper-bound validation is a business rule you'd add explicitly, not something Python enforces for you. - Non-string input types: if
raw_valueisNoneor a list,int(raw_value)raisesTypeError, notValueErrorā and the current code doesn't catchTypeError, so it would propagate up uncaught. This is intentional here to illustrate why matching the exact exception type matters.
Common Mistakes
- Bare
except:with no exception type. This catches everything, including typos in your own code (NameErrorfrom a misspelled variable), and hides them right alongside the error you meant to catch. - Catching the generic
Exceptionclass as a habit. It technically works, but a network timeout and a typo in your code get the exact same vague error message, making debugging much harder later. - Using
exceptandpasstogether with nothing else. This silently swallows failures with zero trace that anything went wrong ā the program appears to work while quietly doing the wrong thing. - Putting too much code inside the
tryblock. If five lines are insidetryand only one of them can actually raise the exception you're catching, you risk masking failures from the other four lines under the same handler. - Forgetting that
elseonly runs after zero exceptions. Some developers put post-success code directly after theexceptblock instead of inelse, which means it also runs even when an exception was caught but didn't cause areturn. - Using
finallyfor logic that should only happen on success or only on failure.finallyruns unconditionally ā it's for cleanup, not conditional business logic. - Catching an exception without re-raising or logging it when the caller genuinely needs to know. Swallowing an error you don't actually know how to handle just relocates the bug somewhere harder to find.
Interview Questions
- What's the difference between
except Exceptionand a bareexcept:? - When would you use
elseinstead of just putting code at the end of thetryblock? - Explain EAFP versus LBYL ā when would you choose one over the other?
- Does
finallyrun if the function returns from inside thetryblock? What if it returns from insideexcept? - How would you design a custom exception hierarchy for a small application?
- What happens if an exception is raised inside a
finallyblock? - Why is catching overly broad exception types considered risky in production code?
Similar Problems
- LeetCode-style input validation problems (e.g., parsing malformed strings to integers) ā same core skill: catching
ValueErrorfromint()/float()conversions. - File I/O problems involving
FileNotFoundErrorandPermissionErrorā reinforce the EAFP pattern with real system resources instead of pure computation. - Dictionary/lookup-heavy problems using
.get()versus direct indexing withKeyErrorā a close cousin of this lesson, since.get()is essentially LBYL and try/except on direct access is EAFP. - Custom exception design problems in larger take-home assignments ā extending this lesson's
InvalidAgeErrorpattern to build a full exception hierarchy for a domain.
Key Takeaways
try holds the risky code. except catches a specific, named failure ā never catch more broadly than you understand. else runs only when the try block succeeded completely, keeping success-path logic cleanly separated from the risky part. finally always runs, making it the right home for cleanup work like closing files or connections. You can also raise your own exceptions to enforce rules specific to your program, turning "invalid data" into a named, catchable event instead of a silent bug. Master this pattern, and your code stops crashing on bad input and starts handling problems the way production software actually needs to.
Watch the Video
This lesson is easier to absorb watching the flow play out step by step ā catch the full walkthrough here: {LINK}
About the Series
This lesson is part of Fun with Learning Technology, a series built around one idea: the concepts that trip up developers the most ā exceptions, recursion, closures, generators ā are almost always simple once you see the right mental model. Each lesson builds on the last, so if exception handling clicked for you here, the rest of the series is designed to keep that momentum going.
Call To Action
If this lesson made try/except click for you, the video has the full walkthrough with live code ā give it a like and subscribe on YouTube so future lessons land in your feed automatically. For deeper dives and written breakdowns like this one, subscribe to the Substack. And if you've got a war story about a bare except: that hid a bug for way too long, drop it in the comments ā those stories are usually the best teachers. Sharing this with a developer who's still fighting scary tracebacks helps the series reach more people who need it.
The solution
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero")
except ValueError:
print("Bad value given")Ready to try it yourself? Solve Python problems with instant feedback in the practice sandbox.
Practice now ā


