Layer 1 Session 11 โ€” Gate Python Foundations

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.

! try raised caught, handled

Something goes wrong โ€” caught deliberately, instead of crashing.

Estimated time: 35โ€“40 minutes  ยท  Pre-quiz โ†’ Concept โ†’ Lab โ†’ Commit โ†’ Post-quiz

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.

Question 1 of 5

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?

An unhandled exception propagates up the call stack, printing a traceback, and stops the program (or whichever thread/request it happened in) once it reaches the top with nothing to catch it.
Question 2 of 5

Given try:\n value = country["capital"]\nexcept KeyError:\n value = "Unknown", what is value if "capital" is missing?

The try block's KeyError is caught by the matching except KeyError: clause, which runs instead of crashing, assigning "Unknown" to value.
Question 3 of 5

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.
Question 4 of 5

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).
Question 5 of 5

Why is except Exception: (catching everything) usually considered a mistake?

Catching everything means a genuine bug โ€” like a typo'd variable name raising 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

TRYrisky code runsEXCEPT KEYERRORhandled here

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
Layer 1 gate: This is the last Layer 1 session. Every remaining layer assumes you can read a traceback, choose the right exception to catch, and know the difference between recoverable and unexpected failures.

4. Lab

Lab objective: Write a safe country lookup function that handles missing keys and invalid input using try/except, else, and finally, plus a function that raises its own exception.

What you will build

A file called errors_lab.py.

Step-by-step instructions

1

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
]
2

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
3

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])
4

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])
5

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

FileActionWhy
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.
If you find yourself editing any other file, stop. This session touches exactly 2 files.

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"
Do not commit until you can answer out loud: "Why is catching KeyError specifically safer than catching Exception broadly?"

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.

Question 1 of 5

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?

An unhandled exception propagates up the call stack, printing a traceback, and stops the program (or whichever thread/request it happened in) once it reaches the top with nothing to catch it.
Question 2 of 5

Given try:\n value = country["capital"]\nexcept KeyError:\n value = "Unknown", what is value if "capital" is missing?

The try block's KeyError is caught by the matching except KeyError: clause, which runs instead of crashing, assigning "Unknown" to value.
Question 3 of 5

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.
Question 4 of 5

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).
Question 5 of 5

Why is except Exception: (catching everything) usually considered a mistake?

Catching everything means a genuine bug โ€” like a typo'd variable name raising 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.

  1. Why does Python prefer "ask forgiveness" (try/except) over "look before you leap" (checking if "capital" in country first) in many cases? Can you think of an argument for each style?
  2. What real production bug could except Exception: pass hide that a specific except KeyError: would not?
  3. 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.
  4. 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_population does 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.