NOETRION

Choose a topic

DAY 17 OF 30 · TRANSFORM AND ORGANIZE

Exceptions and validation

Handle known failures, raise useful errors, and preserve evidence of unexpected bugs.

90–120 minutes suggested study timePython 3.12Not yet completed

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

  1. 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.
  2. 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.
  3. 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.

DAILY CHECKPOINT

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.

01. What belongs in a narrow try block?

02. When does try/else execute its else block?

03. What is finally useful for?

04. Why avoid except Exception: pass around everything?

05. What does raise ValueError communicate here?

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.