Real programs encounter missing files, invalid input, and unavailable services. An exception signals that normal execution cannot continue at a particular point. Your job is to decide which layer can handle the problem usefully. After classes, we can define a small error type and keep parsing separate from the terminal message shown to a user.
Read the evidence before changing code
A syntax error means Python could not parse the program. A runtime exception occurs while executing valid syntax. A traceback shows the calls leading to an unhandled exception, with the final line naming the error and its message. Start there, then find the last relevant line in your own code.
NameError often means a missing or misspelled name. TypeError signals an unsupported operation or call. ValueError often means an argument has the right general kind but an unacceptable value. KeyError means a required mapping key is absent. These are clues to investigate, not interchangeable labels. The exception tutorial demonstrates the main forms.
Translate errors at a useful boundary
Save this complete program as parse_demo.py:
class InputError(ValueError):
"""A study duration cannot be accepted."""
def parse_minutes(raw: str) -> int:
try:
minutes = int(raw)
except ValueError as error:
raise InputError("enter a whole number of minutes") from error
if minutes <= 0:
raise InputError("minutes must be positive")
return minutes
def main() -> None:
for raw in ["25", "oops", "0"]:
try:
minutes = parse_minutes(raw)
except InputError as error:
print(f"Rejected: {error}")
else:
print(f"Accepted: {minutes}")
finally:
print("Checked one value")
if __name__ == "__main__":
main()
Run python parse_demo.py. The accepted value is 25; the other inputs produce their respective rejection messages. Each attempt also prints Checked one value. Here finally makes its execution visible for teaching; in a real application, use it for necessary cleanup rather than routine progress messages.
Only the conversion is inside the inner try. That keeps the handler focused on the operation expected to raise this particular error. raise ... from error preserves the original cause for a traceback while giving callers a domain-specific error. Our InputError inherits from ValueError, so a caller can catch either the specific type or that broader category deliberately.
else runs when the try suite completes without raising an exception. finally runs as control leaves the construct, including during a return or exception. Do not return from finally; doing so can suppress a pending exception or replace a result.
Catch what you can explain
Avoid a bare except and avoid swallowing every Exception with pass. Doing so can disguise programming mistakes as successful work. Catch a specific failure where there is a meaningful recovery, translation, or user-facing message. A missing optional configuration file might have a default; a damaged file holding user data should not silently become an empty database.
Assertions are for internal assumptions while developing. They are unsuitable for validating external input because Python can omit them in optimized execution. Use an explicit condition and exception when the rule must always apply.
Inspect a small failing case
Before opening a debugger, reproduce the issue with the smallest input possible. State the expected value and inspect the actual one. repr(value) is useful when whitespace or the difference between a string and an integer matters. A printed 25 may hide that the value is actually "25".
The standard-library debugger, pdb, lets you pause and inspect state. Launch the example from the terminal:
python -m pdb parse_demo.py
At the debugger prompt, use break parse_minutes to set a breakpoint, continue to run there, p raw to inspect the argument, and next to execute the next line without entering its calls. where shows the call stack; quit exits. Alternatively, temporarily insert breakpoint() immediately before a suspicious operation, then remove it after investigation.
Pitfalls and practice
Do not “repair” errors by changing several unrelated lines at once. Preserve the failing input and add a regression test after understanding the cause. Avoid logging secrets or entire private records merely to inspect one field.
Exercise: extend the parser to reject values above 240, while keeping the existing positive-integer rule. Predict the result for "240", "241", and "2.5".
Solution guidance: check the upper bound after conversion and the lower-bound check. Accept 240, reject 241 with a duration-specific message, and let decimal text follow the conversion-error path. Keep the terminal reporting in main; the parser should remain reusable by tests and other interfaces.

