Errors and Exceptions
This is the Layer 1 gate. Real data is messy โ missing keys, wrong types, invalid input. We learn to handle failure deliberately instead of letting the whole program crash.
Something goes wrong โ caught deliberately, instead of crashing.
1. Learning Objective
By the end of this session you will be able to:
- Explain what an exception is and how it differs from a normal return value
- Use try/except to catch a specific exception type and recover
- Use else and finally correctly
- Raise your own exception with a clear message using raise
- Explain why catching Exception broadly is usually a mistake
2. Pre-Coding Quiz
Answer these before reading the concept explanation or writing any code. It is fine to get these wrong โ that is the point. You need 4/5 to proceed to the lab.
What happens when Python hits an error like country["capital"] on a dict with no "capital" key, if there is no try/except around it?
Given try:\n value = country["capital"]\nexcept KeyError:\n value = "Unknown", what is value if "capital" is missing?
try block's KeyError is caught by the matching except KeyError: clause, which runs instead of crashing, assigning "Unknown" to value.What is the difference between an except block's code and a finally block's code?
except is conditional on a matching exception being raised. finally is unconditional โ it always executes, useful for cleanup like closing a file, regardless of whether an error happened.You write raise ValueError("population must be positive") inside a function. What happens?
raise deliberately triggers an exception. Execution of the current function stops immediately at that line, and the exception propagates up until something catches it (or the program crashes with that message).Why is except Exception: (catching everything) usually considered a mistake?
NameError โ gets silently treated the same as an expected, recoverable error. This hides real problems instead of surfacing them. Catch the specific exception type you actually expect.3. The Concept โ Errors and Exceptions
A caught exception is handled locally; an uncaught one keeps propagating up the call stack.
What is an exception?
When something goes wrong, Python does not silently return a bad value โ it raises an exception, which immediately stops normal execution and propagates upward looking for something to handle it.
country = {"name": "Kenya"}
print(country["capital"])
# Traceback (most recent call last):
# ...
# KeyError: 'capital'
# Program stops here unless something catches it
Catching a specific exception with try/except
Wrap the risky code in try, and handle the specific failure in a matching except. Only exceptions of that type (or a subclass) are caught โ everything else still propagates.
country = {"name": "Kenya"}
try:
capital = country["capital"]
except KeyError:
capital = "Unknown"
print(capital) # "Unknown" โ no crash
else and finally
An optional else block runs only if the try block succeeded with no exception. An optional finally block always runs, whether or not an exception occurred โ used for cleanup.
try:
capital = country["capital"]
except KeyError:
print("No capital on file")
else:
print("Found capital:", capital) # only runs if no exception
finally:
print("Lookup attempt finished") # always runs
Raising your own exceptions
You are not limited to reacting to Python's built-in errors. Use raise to signal that your own code has hit an invalid state, with a message explaining what went wrong.
def set_population(country, value):
if value < 0:
raise ValueError(f"population must be positive, got {value}")
country["population"] = value
country = {"name": "Kenya"}
set_population(country, 54000000) # fine
set_population(country, -5)
# Traceback (most recent call last):
# ...
# ValueError: population must be positive, got -5
Catch specific exceptions, not everything
Catching a broad Exception hides real bugs by treating every kind of failure โ including ones you never intended to handle โ the same way. Always catch the narrowest exception type that matches what you actually expect to go wrong.
# Risky: hides a typo (NameError) behind the same fallback as a real missing key
try:
value = coutnry["capital"] # typo! raises NameError, not KeyError
except Exception:
value = "Unknown" # bug is silently hidden
# Better: only catch what you actually expect
try:
value = country["capital"]
except KeyError:
value = "Unknown" # a real typo would now crash loudly, as it should
4. Lab
What you will build
A file called errors_lab.py.
Step-by-step instructions
Create the file and the country data
# errors_lab.py
countries = [
{"name": "Kenya", "region": "Africa", "population": 54000000},
{"name": "Ghana", "region": "Africa"}, # note: no population key
]
Write a function that reads population safely with try/except
def get_population(country):
try:
return country["population"]
except KeyError:
return None
print(get_population(countries[0])) # 54000000
print(get_population(countries[1])) # None โ no crash
Add else and finally to see the full flow
def report_population(country):
try:
pop = country["population"]
except KeyError:
print(country["name"], "has no population on file")
else:
print(country["name"], "population is", pop)
finally:
print("Lookup complete for", country["name"])
report_population(countries[0])
report_population(countries[1])
Write a function that raises ValueError on invalid input
def set_population(country, value):
if not isinstance(value, int) or value < 0:
raise ValueError(f"population must be a non-negative int, got {value!r}")
country["population"] = value
set_population(countries[1], 31000000) # fine
print(countries[1])
Call set_population with bad input inside a try/except and confirm it is caught
try:
set_population(countries[1], -5)
except ValueError as e:
print("Rejected:", e)
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
errors_lab.py |
Created | Demonstrates try/except/else/finally and raising a custom ValueError. |
docs/sessions/session-11/index.html |
Created | This session document โ Layer 1 gate. |
6. Commit Checkpoint
Once the lab is complete and you can explain every line, make this exact commit:
git add errors_lab.py docs/sessions/session-11/index.html
git commit -m "session-11: handle missing keys with try/except and raise ValueError on invalid input"
7. Code Review Checklist
Go through your code line by line and check each item:
8. Post-Coding Quiz
Same 5 questions. Take it again now that you have written and run the code. You need 4/5 to mark this session complete.
What happens when Python hits an error like country["capital"] on a dict with no "capital" key, if there is no try/except around it?
Given try:\n value = country["capital"]\nexcept KeyError:\n value = "Unknown", what is value if "capital" is missing?
try block's KeyError is caught by the matching except KeyError: clause, which runs instead of crashing, assigning "Unknown" to value.What is the difference between an except block's code and a finally block's code?
except is conditional on a matching exception being raised. finally is unconditional โ it always executes, useful for cleanup like closing a file, regardless of whether an error happened.You write raise ValueError("population must be positive") inside a function. What happens?
raise deliberately triggers an exception. Execution of the current function stops immediately at that line, and the exception propagates up until something catches it (or the program crashes with that message).Why is except Exception: (catching everything) usually considered a mistake?
NameError โ gets silently treated the same as an expected, recoverable error. This hides real problems instead of surfacing them. Catch the specific exception type you actually expect.9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Why does Python prefer "ask forgiveness" (try/except) over "look before you leap" (checking
if "capital" in countryfirst) in many cases? Can you think of an argument for each style? - What real production bug could
except Exception: passhide that a specificexcept KeyError:would not? - In the lab, what would happen if you called
set_population(countries[1], "fifty")โ a string instead of a number? Trace through the isinstance check. - How does raising your own exception with a clear message help the next person (including future you) debug faster than a generic crash?
10. What Breaks If This Knowledge Is Missing?
- Silent data corruption: Catching too broadly (or not catching at all) means one bad record in a dataset can either crash your entire program or, worse, silently produce wrong results that are hard to trace back to their cause.
- Class validation (Layer 2): In Session 13, class constructors will validate their inputs the same way
set_populationdoes here โ raising a clear exception is how a class protects itself from being created in an invalid state. - Real API calls (Layer 7): In Session 42, a real network call can fail for a dozen reasons โ no connection, bad response, rate limiting. Without this session, you cannot handle any of them gracefully, and one flaky network call would crash the whole application.
11. What We Learned
Python concept mastered: Exceptions โ try/except/else/finally, raising your own exceptions with raise, and why catching narrowly matters.
Unlocks: You can now write code that fails safely and predictably instead of crashing on the first messy input โ the last Layer 1 skill before we start modelling real objects.
Next session: Session 12 โ What Classes Are and Why. Layer 2 begins. We stop passing raw dictionaries around and start modelling a Country as a class.