Learn
05.3 | VALIDATION AND EXCEPTION HANDLING
01 | A CONVERSION IS NOT THE WHOLE CHECK
int("-4") succeeds, but a negative age or expense might still break your requirements. int("four") fails before a range comparison can happen. These are different problems: conversion interprets text; validation checks whether the resulting data is acceptable.
Start with rules. For a small ticket quantity, you might allow whole numbers from zero through twenty. A blank response fails presence; "2.5" fails the integer-text conversion; -1 and 21 fail the range. Zero can be valid, so a truthiness check alone is unsuitable.
Validation does not prove factual accuracy. A plausible date and permitted expense amount may still be entered incorrectly. A user review can help catch that separate problem.
02 | USE CHECKS THAT MATCH THE REQUIREMENT
A presence check asks whether required input exists. A type check asks what kind of value it is. A range check checks limits. A format check checks structure, such as four digits, hyphen, two digits, hyphen, two digits. A membership check restricts a category to an allowed collection.
A date matching that structure may still be impossible. 2026-02-30 looks like YYYY-MM-DD but is not a calendar date. Use the calendar parser as well as a strict format rule when your requirements need both.
Do not strip every field automatically. Outer whitespace may be irrelevant to a menu command but meaningful in some data. State where normalisation is intentional before validating.
03 | AN EXCEPTION INTERRUPTS NORMAL FLOW
An exception is a signal that normal execution cannot continue as requested. int("four") raises ValueError. Opening a missing file for reading raises FileNotFoundError. Accessing a missing dictionary key raises KeyError.
When an exception occurs, Python searches for a matching handler. If none handles it, a traceback describes the call path and the program stops. The final traceback line usually names the exception and message; earlier lines help locate its origin.
Do not catch an exception merely to erase its evidence. Catch it at a place that can recover, give a useful explanation or stop cleanly. A hidden exception can make a damaged program appear to have worked.
04 | KEEP THE TRY BLOCK FOCUSED
The try block contains operations that may fail in a way you intend to handle. An except ValueError block handles that specific exception type, including its subclasses. Exceptions of other types continue outward unless another handler matches.
Keep unrelated calculations outside the try block where practical. If too much code sits inside one broad handler, a genuine internal bug may be reported as bad user input.
The example converts a response, reports conversion failure, then separately checks the range. Its input is a known string, which fits the int conversion contract.
text = "21"
try:
quantity = int(text)
except ValueError:
print("Enter a whole number")
else:
if 0 <= quantity <= 20:
print("Accepted:", quantity)
else:
print("Use a quantity from 0 to 20")EXPECTED OUTPUT
Use a quantity from 0 to 20
05 | ELSE AND FINALLY HAVE DIFFERENT PURPOSES
The else block of try runs when the try block completes without an exception. It is useful for work that depends on conversion succeeding. An exception from else is not handled by that same try statement’s except clauses.
finally normally runs as execution leaves the try structure, whether the operation succeeds or an exception propagates. It suits cleanup, but a file context manager is usually clearer for closing files. finally does not imply that the operation succeeded.
Avoid return inside finally in these examples: it can replace an earlier result or suppress an exception. Cleanup should not quietly change the meaning of success or failure.
try:
count = int("7")
except ValueError:
print("Conversion failed")
else:
print("Converted:", count)
finally:
print("Attempt finished")EXPECTED OUTPUT
Converted: 7 Attempt finished
06 | RAISE AN ERROR AT THE FUNCTION BOUNDARY
A reusable validation function can raise ValueError when a supplied value is unsuitable. Its caller decides whether to retry, report failure or stop. This keeps input prompts separate from the validation rule.
A deliberate raise communicates the contract clearly. Returning zero for every invalid quantity would be ambiguous when zero is itself a permitted quantity.
Use exception chaining with raise ValueError(...) from error when replacing a low-level message while retaining the original cause. It makes debugging possible without requiring the user-facing prompt to reproduce a technical traceback.
def parse_quantity(text):
try:
quantity = int(text)
except ValueError as error:
raise ValueError("Enter a whole-number quantity") from error
if not 0 <= quantity <= 20:
raise ValueError("Quantity must be from 0 to 20")
return quantity
print(parse_quantity("0"))EXPECTED OUTPUT
0
07 | RETRY WITHOUT CHANGING ACCEPTED STATE
A retry loop should collect and validate a proposed value before changing the accepted data. For a quiz, invalid input should leave the current question and score unchanged. For an expense, invalid input should not append a partly formed record.
Prepare a candidate, validate it and only then commit the change. In the project, saving must also succeed before the in-memory list is replaced by the candidate. Otherwise the screen could claim an addition that did not reach the file.
A retry loop needs a successful exit or a cancellation route. Do not repeatedly test the same invalid value without reading a new response.
def parse_quantity(text):
value = int(text)
if not 0 <= value <= 20:
raise ValueError("Use 0 to 20")
return value
responses = ["bad", "21", "3"]
for text in responses:
try:
quantity = parse_quantity(text)
except ValueError:
print("Rejected:", text)
else:
print("Accepted:", quantity)
breakEXPECTED OUTPUT
Rejected: bad Rejected: 21 Accepted: 3
08 | CATCH THE FAILURE YOU CAN ADDRESS
FileNotFoundError is a kind of OSError. If separate handlers need different behaviour, put the more specific handler first. A broad OSError handler before FileNotFoundError would already catch that missing-file case.
A missing optional save file may justify returning an empty list. Permission failures or malformed existing data do not justify that same response. Catch only the cases you have deliberately designed to recover from.
except Exception can be useful at a carefully designed application boundary, but it should not become a blanket replacement for specific handlers. A bare except also catches signals such as KeyboardInterrupt, which users may expect to stop a program.
09 | TRACE CONVERSION AND RANGE SEPARATELY
The fixed model accepts ASCII integer text with an optional sign and a quantity from zero through twenty. It intentionally uses a stricter text grammar than every form Python int() can accept. The trace shows conversion, range checking and commitment as separate stages.
10 | LIMITS ARE PART OF THE DESIGN
A useful handler gives an actionable message: which rule failed and what is allowed. It should not invent a successful default merely to keep the program running. Keep the original file and accepted state intact when recovery is uncertain.
For Decimal or float input, consider non-finite values such as NaN and infinity if you permit that conversion. The project’s restricted decimal-text grammar excludes them before converting to integer cents.
Test each rule separately, then combine them. An ordinary valid value checks the happy path; blank, malformed, just-outside and exact-boundary cases expose different branches. More prompts are not a substitute for validation.
PUT YOUR SKILLS TO WORK
Use the Section 5 challenges to practise these ideas independently. Work in your Python editor, predict results and keep a short test record.
Section 5 challenges Python course navigation