Today's outcomes
- Trace try, except, else, and finally.
- Validate inputs with explicit exceptions.
- Keep conversion failures separate from valid zero values.
Before you start: Complete Day 16 and its practical challenge.
Understand the concept
An exception reports a failure that interrupts ordinary flow
A syntax error prevents a program from being parsed correctly. An exception occurs during execution when an operation cannot complete normally. ValueError can report unsuitable input for a conversion; TypeError can report an unsupported kind of value; FileNotFoundError can report an absent required file.
An exception's type and message are evidence. Replacing every exception with zero or an empty list discards that evidence and can turn a failed import into a convincing report. Handle only the failures your contract understands. Unexpected programming mistakes should remain visible until they are investigated.
Try, except, else, and finally divide responsibilities
The try block contains operations whose expected failures you intend to handle. An except clause handles matching exceptions. The else block runs when the try block finishes without an exception. The finally block runs as execution leaves the structure, including during ordinary propagation.
Keep the try block narrow. If it includes conversion, unrelated calculation, and display, a handler for ValueError may accidentally hide a failure from a different operation. Put the known risky conversion inside try and perform ordinary subsequent work in else or after the handler.
For file resources, a with statement is usually clearer than manually opening a file and closing it in finally. Finally remains useful for other cleanup or observation. Avoid returning from finally: that can suppress an exception or replace an earlier return value, making the original failure disappear.
Validation defines the accepted domain explicitly
Conversion and validation are not the same step. int('150') succeeds, but 150 might be outside a score range. int(3.9) succeeds by truncating, which may violate a whole-number input requirement. bool is a subclass of int, so isinstance(True, int) succeeds. If a field requires an actual integer excluding Booleans, use a check that expresses that distinction.
Today's duration parser accepts only strings containing whole-number text that int can parse, including surrounding whitespace. Its result must be from 0 through 240 inclusive. Other input types raise TypeError, conversion problems raise ValueError, and out-of-range values raise ValueError with a useful message.
Worked example
def parse_minutes(text):
if not isinstance(text, str):
raise TypeError("minutes input must be text")
try:
minutes = int(text)
except ValueError as error:
raise ValueError("minutes must be whole-number text") from error
if not 0 <= minutes <= 240:
raise ValueError("minutes must be between 0 and 240")
return minutes
for raw in ["45", "0", " 60 ", "3.5", "-1"]:
try:
value = parse_minutes(raw)
except ValueError as error:
print(repr(raw), "rejected:", error)
else:
print(repr(raw), "accepted:", value)
| Input | Outcome | Reason |
|---|---|---|
"45" |
45 | Accepted integer text and range |
"0" |
0 | Known zero is within the domain |
" 60 " |
60 | int accepts surrounding whitespace |
"3.5" |
ValueError | Not accepted whole-number text |
"-1" |
ValueError | Conversion succeeds, range fails |
| True | TypeError | Input contract requires text |
The from error clause preserves the original conversion exception as the cause of the more specific message. It helps debugging without asking the caller to interpret the implementation's raw conversion details.
Guided practice
- Predict: which handler runs for an absent file, an invalid integer string, and a misspelled variable? Explain why one broad “return zero” handler would conflate unrelated situations.
- Modify: add an upper-bound test at 240 and an excluded case at 241. A function that handles 45 correctly has not established correct boundary behavior.
- Reconstruct: write a function accepting an actual integer score, excluding Booleans, from 0 through 100. Use explicit exceptions rather than assert statements for input validation.
Test failures as failures. It is not enough to check that an invalid input produces “some result.” Inspect the expected exception type and verify the code did not continue with a fabricated quantity.
Daily challenge
Implement audit_inputs(inputs) using parse_minutes. Accept a list of arbitrary objects. Return an accepted integer list and a rejection list. Each rejection should include the original position, an error type name, and a useful message. Handle only TypeError and ValueError from the parser. Preserve input order for both result lists.
| Input fixture | Accepted | Rejected positions |
|---|---|---|
['30', 'bad', '-1', '0', True] |
[30, 0] |
1, 2, 4 |
[] |
[] |
none |
['240', '241'] |
[240] |
1 |
The original input contains five observations in the first case. Two accepted plus three rejected must reconcile to five. Do not include a fake zero for the rejected text “bad.”
Reference solution
Compare after attempting the challenge
def parse_minutes(text):
if not isinstance(text, str):
raise TypeError("minutes input must be text")
try:
value = int(text)
except ValueError as error:
raise ValueError("minutes must be whole-number text") from error
if not 0 <= value <= 240:
raise ValueError("minutes must be between 0 and 240")
return value
def audit_inputs(inputs):
accepted, rejected = [], []
for position, raw in enumerate(inputs):
try:
accepted.append(parse_minutes(raw))
except (TypeError, ValueError) as error:
rejected.append({"position": position, "type": type(error).__name__,
"message": str(error)})
return accepted, rejected
accepted, rejected = audit_inputs(["30", "bad", "-1", "0", True])
assert accepted == [30, 0]
assert [item["position"] for item in rejected] == [1, 2, 4]
assert rejected[-1]["type"] == "TypeError"
assert len(accepted) + len(rejected) == 5
assert audit_inputs([]) == ([], [])
assert audit_inputs(["240", "241"])[0] == [240]
print("Validation audit passed")
enumerate supplies a zero-based source position alongside each item. It gives the rejection report an explicit link to the original list. For a real imported file, an appropriate row identifier or source line may be more useful.
Debug and explain
Keep the failing input and full traceback while debugging. A handler printing “error” without a type or context is less useful than a targeted message. Avoid logging secrets merely to gain context; record the field and failure category instead when the input is sensitive.
Explain the difference between conversion failure and range failure, why True is rejected, and why the accepted list retains a genuine zero. Then identify an unexpected exception this audit should not hide.
Review and next step
Save normal, boundary, wrong-type, and invalid-text cases. Rebuild the parser from its contract during your next review. Day 18 organizes reusable functions into modules so validation does not have to be copied across scripts.
Sources and verification
Original teaching examples checked with Python 3.12.14. Library behavior reviewed against these primary references on October 3, 2026. Day 26's examples also use Beautiful Soup 4.14.3.
Check your understanding
Five questions, 20 points each. Pass at 85% or above, which requires all five correct. Feedback explains each answer; revise the lesson and retry when needed.
Review your practical work
Check these statements after running your own challenge. This is your self-review; the website does not execute or assess your Python code.
Pass the knowledge check and finish the practical self-review to save completion.