JEPA4Japan · tutorials

Exceptions and Input Validation

1,140 words 6 min read #Python

Detect invalid states, recover from expected failures, and report useful errors.

Course progress Course outline 24 of 24 lessons available

An exception is an alarm

Chapter 9 gave each function a contract. What if an input breaks that promise? Python stops the current path and raises an exception. That is useful information, not a monster.

  1. Stop an operation cannot finish
  2. Read start at the last line
  3. Name it type plus message
  4. Choose fix, reject, or recover
A traceback tells you what failed and where to look.

This line intentionally fails:

minutes = int("forty")

The traceback ends with a ValueError message. Read that final line first, then move upward to the line in your file. Different types point to different broken promises: int(None) raises TypeError, and 10 / 0 raises ZeroDivisionError.

Catch only the alarm you understand

A try block should be tiny. Put only the risky operation you intend to handle inside it.

raw_minutes = "forty"

try:
    minutes = int(raw_minutes)
except ValueError:
    print("Minutes must be a whole number.")
else:
    print("Accepted:", minutes)

Output:

Minutes must be a whole number.

The except ValueError is narrow: it handles the conversion failure it understands. Avoid bare except: and avoid catching Exception merely to keep moving. They can hide spelling mistakes and broken calculations.

Keep later formatting in else. Then a bug in that formatting cannot be mislabeled as bad input. Catch several types together only when the boundary truly treats them the same, such as except (TypeError, ValueError) for text-or-None input.

Four paths have four jobs

  1. try attempt one risky step
  2. except handle one expected failure
  3. else use a successful result
  4. finally always clean up
Each suite describes a different route through the program.
def show_average(total, count):
    try:
        average = total / count
    except ZeroDivisionError:
        print("No sessions to average.")
    else:
        print(f"Average: {average:.1f}")
    finally:
        print("Calculation finished.")


show_average(90, 3)
show_average(0, 0)

Output:

Average: 30.0
Calculation finished.
No sessions to average.
Calculation finished.

else runs only after success. finally runs after success, a caught exception, or an uncaught exception. Use it for real cleanup, not to hide an error or replace a return value. Chapter 12 will show a simpler cleanup tool for files.

Validation is a gate, not a guess

Converting a value and checking its meaning are two separate gates. "0" can become the integer 0, but zero may still be forbidden.

  1. Raw input "45" or "many"
  2. Convert can it become an integer?
  3. Validate is 1 through 1440 allowed?
Only data that passes both gates becomes trusted data.
def parse_minutes(raw_minutes):
    try:
        minutes = int(raw_minutes)
    except (TypeError, ValueError) as cause:
        raise ValueError("minutes must be a whole number") from cause

    if not 1 <= minutes <= 1440:
        raise ValueError("minutes must be from 1 to 1440")
    return minutes

raise ... from cause gives callers a stable, helpful message while preserving the original conversion failure in error.__cause__ and the traceback.

Use TypeError when the kind of object breaks the interface, and ValueError when the kind is accepted but its value is not. Do not silently invent zero for required bad data. Small edge: int(True) becomes 1; reject Boolean values explicitly if your contract says they are not minutes.

Build a trusted study gate

Save this complete project as study_gate.py:

def normalize_topic(raw_topic):
    if not isinstance(raw_topic, str):
        raise TypeError("topic must be text")
    topic = raw_topic.strip()
    if not topic:
        raise ValueError("topic must not be empty")
    return topic


def parse_minutes(raw_minutes):
    if isinstance(raw_minutes, bool):
        raise TypeError("minutes must not be a Boolean")
    try:
        minutes = int(raw_minutes)
    except (TypeError, ValueError) as cause:
        raise ValueError("minutes must be a whole number") from cause
    if not 1 <= minutes <= 1440:
        raise ValueError("minutes must be from 1 to 1440")
    return minutes


def make_session(raw_topic, raw_minutes):
    return {
        "topic": normalize_topic(raw_topic),
        "minutes": parse_minutes(raw_minutes),
    }


def process_entries(raw_entries):
    accepted = []
    problems = []
    for number, (topic, minutes) in enumerate(raw_entries, start=1):
        try:
            session = make_session(topic, minutes)
        except (TypeError, ValueError) as error:
            problems.append(f"Entry {number}: {error}")
        else:
            accepted.append(session)
    return accepted, problems


def format_report(sessions, problems):
    total = 0
    for session in sessions:
        total += session["minutes"]
    lines = [
        f"Accepted: {len(sessions)}",
        f"Rejected: {len(problems)}",
        f"Trusted minutes: {total}",
    ]
    for problem in problems:
        lines.append("- " + problem)
    return "\n".join(lines)


def run_checks():
    assert normalize_topic(" Python ") == "Python"
    assert parse_minutes("45") == 45
    try:
        parse_minutes("many")
    except ValueError as error:
        assert str(error) == "minutes must be a whole number"
        assert isinstance(error.__cause__, ValueError)
    else:
        raise AssertionError("bad minutes were accepted")


run_checks()
raw_entries = [
    (" Python ", "45"),
    ("Git", "many"),
    ("   ", "15"),
    ("SQL", "30"),
    ("Testing", "0"),
]
sessions, problems = process_entries(raw_entries)

print("Checks passed.")
print(format_report(sessions, problems))

Run python3 study_gate.py:

Checks passed.
Accepted: 2
Rejected: 3
Trusted minutes: 75
- Entry 2: minutes must be a whole number
- Entry 3: topic must not be empty
- Entry 5: minutes must be from 1 to 1440

The collection boundary catches only the validation failures it expects. A programming defect still stays visible instead of being called “bad input.”

Three tiny missions

  1. Write parse_rating() for whole numbers 1 through 5 and test 0, 1, 5, and 6.
  2. Remove the minutes key from a dictionary; catch only KeyError, and put successful printing in else.
  3. Add (42, "10") and ("Git", True) to the project data and predict both messages.

Ready for Chapter 11?

  • I read a traceback from its final line.
  • I keep try small and catch only expected exception types.
  • I can explain except, else, and finally.
  • I convert first, then validate meaning.
  • I use raise ... from ... to preserve a cause.
  • I ran the Study Gate and all checks passed.

Next, you will move these trusted helpers into modules and packages and give each project its own environment.