AmouAI Hub/Courses/Programming Fundamentals/Day 15
Exceptions, Testing & Mini-Project 3
Failing well, and proving you did. Tests are cheaper than debugging, and this is where that stops being a slogan.
By the end of today you can
- Use
try/except/else/finally, and say what each part is for - Catch the narrowest exception that makes sense, and never a bare
except raisedeliberately, with a message a stranger could act on- Trace an exception up the call stack and predict where it is caught
- Write a custom exception class and say when it earns its place
- Write a
pytesttest, run it, and read the failure output - Test the edges — empty, one, negative, wrong type
- Deliver Mini-Project 3
▶Today's videos
Watch each video, then work the matching sections below. Watching alone will not do it.
try/except/else/finally and what each is for · catching narrowly · why a bare except manufactures silent bugs · raising deliberately with an actionable message · propagation up the stack · custom exceptions that earn their place.pytest conventions · reading a failure · testing the edges first · pytest.raises · why every testable function in this course returns a value · the Library System build.1try, except, and the ones people forget
Four clauses. Most people use two.
def read_config(path):
try:
f = open(path, encoding="utf-8")
except FileNotFoundError:
print(f"no config at {path}, using defaults")
return {}
else:
# runs only if NOTHING was raised
return parse(f.read())
finally:
# runs no matter what — success, failure, or return
print("config load attempted")| Clause | Runs when | Use it for |
|---|---|---|
try | Always | The risky operation — keep it short |
except | That exception was raised | Recovering, or reporting |
else | Nothing was raised | The follow-on work, so it is not inside try |
finally | Always, even on return | Cleanup you cannot skip |
else matters more than it looksAnything you put in the try block is protected by the except. If parse() happens to raise a FileNotFoundError of its own, you would silently swallow it and return {}. Moving it to else means the try covers exactly one line — the one you meant.
Catch narrowly
except: pass converts a loud, locatable failure into a silent wrong answer. Day 5 taught you that semantic errors are the expensive ones — a bare except is a machine for manufacturing them.
2raise, and propagation
An exception travels up until something catches it.
Raise where the problem is detected. Catch where you know what to do about it. parse_age knows the string is bad but not whether that should stop the program. main knows the policy but not the details. Exceptions carry the news between them.
def parse_age(raw):
"""Age as an int. Raises ValueError if it is not sensible."""
age = int(raw) # may raise ValueError itself
if age < 0 or age > 130:
raise ValueError(f"age out of range: {age}")
return ageTwo things about that message. It says what was wrong and shows the offending value — "age out of range: 214" is actionable; "bad input" is not. And the function raises rather than printing, so a caller can decide what to do.
Custom exceptions
class ContactBookError(Exception):
"""Anything this program raises deliberately."""
class DuplicateContact(ContactBookError):
"""A contact with that name already exists."""
# now a caller can be precise about what it handles
try:
book.add(contact)
except DuplicateContact:
book.update(contact)A custom exception earns its place when a caller needs to tell your failure from a built-in one. If nobody will ever catch it specifically, raise ValueError and save yourself the class.
3pytest
The known-answer habit from Day 5, written down and run automatically.
You have been testing since Day 1 — running a program with values whose answer you already knew. A test is that, saved to a file so the computer does it for you every time.
from stats import mean_of, count_above
def test_mean_of_typical():
assert mean_of([12, 7, 25, 3]) == 11.75
def test_mean_of_single():
assert mean_of([5]) == 5
def test_count_above_is_strict():
"""25 is not ABOVE 25."""
assert count_above([12, 25], 25) == 0
def test_count_above_none_match():
assert count_above([1, 2], 100) == 0pip install pytest
pytest -q| Convention | Rule |
|---|---|
| File name | test_*.py |
| Function name | test_* |
| Assertion | Plain assert — pytest rewrites it to show the values |
| Where | A tests/ folder next to your code |
Reading a failure
def test_count_above_is_strict():
"""25 is not ABOVE 25."""
> assert count_above([12, 25], 25) == 0
E assert 1 == 0
E + where 1 = count_above([12, 25], 25)
test_stats.py:13: AssertionErrorpytest shows the expression, the values it computed, and the line. That last line — where 1 = count_above([12, 25], 25) — is doing the work a print statement would have done, automatically.
The edges first. Empty. One item. Zero. Negative. The maximum. The boundary value itself and the one either side of it. Bugs live at boundaries — a test of the obvious middle case rarely finds anything.
Look at what is testable: mean_of, count_above, format_report. All of them return values. A function that prints cannot be tested — there is nothing to assert on. The return-versus-print distinction was never about style; this is what it was for.
4Mini-Project 3 — Tested Library System
Your third graded deliverable · 100 points
Everything from Week 3 in one program: classes, encapsulation, inheritance, exceptions and a real test suite.
Required behaviour
- A
LibraryItembase class, withBook,DVDandMagazinesubclasses. - Each subclass has a different loan period and a different late-fee rule.
- A
Libraryclass holding items, withcheck_out,return_itemandsearch. - Invariants enforced in one place: no negative fees, no double check-out, no unknown item.
- Custom exceptions —
ItemUnavailable,ItemNotFound— raised deliberately. - Data persists to a file between runs.
- A pytest suite with at least 15 tests, covering every subclass and every raised exception.
| Criterion | Points |
|---|---|
| Class design — sensible hierarchy, honest is-a | 20 |
Encapsulation — invariants enforced, @property where it earns its place | 15 |
| Polymorphism — one call, different behaviour, no type-checking if-chain | 15 |
| Exceptions — raised deliberately, caught narrowly | 15 |
| Test suite — 15+ tests, edges covered | 25 |
| Naming, structure, docstrings, PEP 8 | 10 |
This is deliberate, and it reflects reality: a working program with no tests is worth less than a slightly smaller one you can change safely. Write the test for each behaviour before or immediately after you write the behaviour — not in a panic at the end.
Suggested build order
LibraryItemand one subclass. Two tests. Run pytest and watch them pass.- Add the other two subclasses. Test that the loan periods genuinely differ.
Librarywithcheck_out. Test the happy path, then the double-check-out failure.- Custom exceptions. Test each one with
pytest.raises. - Persistence. Test a save-then-load round trip.
- Edges: empty library, unknown item, zero-day loan, negative fee attempt.
black,ruff, and read it once as a stranger.
import pytest
def test_double_checkout_raises():
library = Library()
book = Book("Think Python", 292)
library.add(book)
library.check_out(book)
with pytest.raises(ItemUnavailable):
library.check_out(book)pytest.raises is how you assert that something must fail. A test suite that only checks the happy path is testing half a program — and the half that was already working.
>_Python playground
A real Python interpreter running inside your browser. Nothing is installed, nothing is uploaded, nothing can break.
input(), comma separated →
✓Exercise set
Checked automatically the moment you submit. Work top to bottom — each one assumes the last. Your answers are saved in this browser.
Day 16 — Java: The Model Shift
Every word of public static void main, explained.