Validation and errors: reject bad data without hiding the reason
Separate missing, malformed, and out-of-range inputs, then keep an auditable record of rejected rows.
Learning lesson · Reviewed October 2, 2026. Allow additional time for the practice. Prerequisite: Dictionaries and records. Python 3; no extra packages.
- Validate a small input contract explicitly.
- Catch a specific conversion error.
- Keep rejection reasons and reconcile all input records.
Real inputs often arrive as text. A field may contain "82", an empty string, or "eighty". Converting every field and hoping for the best either stops the workflow or tempts you to replace errors with zero. This lesson builds a narrow contract for fictional assessment scores: accept whole-number text from 0 to 100, and return a reason when the input does not meet it.
Make the contract visible
Our function expects a string. It strips surrounding whitespace, distinguishes a missing field from malformed text, and checks the range after conversion. It deliberately rejects "82.5" because this exercise uses whole-number scores. A different dataset might allow decimal scores; the policy must follow the data definition rather than convenience.
def parse_score(text):
cleaned = text.strip()
if cleaned == "":
return None, "missing"
try:
score = int(cleaned)
except ValueError:
return None, "not an integer"
if score < 0 or score > 100:
return None, "outside 0-100"
return score, None
for text in [" 82 ", "", "eighty", "101", "0"]:
print(repr(text), parse_score(text))
# ' 82 ' (82, None)
# '' (None, 'missing')
# 'eighty' (None, 'not an integer')
# '101' (None, 'outside 0-100')
# '0' (0, None)The returned tuple contains the value and a reason. A reason of None means the row passed these checks. The try block covers only conversion, and except ValueError handles its known failure. Unexpected failures should remain visible. Catching every exception and returning zero would conceal programming errors as believable data.
Strip whitespace→Presence
Empty?→Type
Integer?→Range
0–100?
Keep rejected records inspectable
Use the parser within a loop that separates accepted values from rejection reasons. The original strings remain available for review; the program does not overwrite the source dataset. These teaching inputs contain no personal information.
raw = ["82", "", "eighty", "101", "0"]
accepted = []
rejected = []
for row_number, text in enumerate(raw, start=1):
value, reason = parse_score(text)
if reason is None:
accepted.append(value)
else:
rejected.append({"row": row_number, "reason": reason})
print(accepted) # [82, 0]
print(rejected)
# [{'row': 2, 'reason': 'missing'},
# {'row': 3, 'reason': 'not an integer'},
# {'row': 4, 'reason': 'outside 0-100'}]
assert len(accepted) + len(rejected) == len(raw)The last check reconciles the processing outcome: two accepted plus three rejected equals five inputs. It does not prove every classification is correct, but it exposes accidentally lost records. Check the classifications separately with known examples. Logging only a row number and reason can also reduce unnecessary exposure compared with printing every original field.
| Input | Expected status | Reason for the test |
|---|---|---|
| "0" | Accepted as 0 | Valid lower boundary |
| "100" | Accepted as 100 | Valid upper boundary |
| "-1" | Rejected | Below allowed range |
| "101" | Rejected | Above allowed range |
| "82.5" | Rejected | Contract requires integer text |
Read an error as evidence
When debugging, identify the exception type, its message, and the relevant line in the traceback. Reproduce the failure on one small input. If parse_score(None) fails, that reveals an input outside our stated string contract. Decide whether to enforce that contract upstream or extend the function deliberately; do not add a broad exception handler solely to make the error disappear.
Practice: test all boundaries
Write checks for zero, 100, -1, 101, whitespace-only text, and a decimal string. Then explain why converting an empty string to zero would change the meaning of a mean.
Show the worked solution
assert parse_score("0") == (0, None)
assert parse_score("100") == (100, None)
assert parse_score("-1") == (None, "outside 0-100")
assert parse_score("101") == (None, "outside 0-100")
assert parse_score(" ") == (None, "missing")
assert parse_score("82.5") == (None, "not an integer")An invented zero increases the denominator and changes the total used for the mean. Missing is an absence of observation, while zero is an observation with a specific value.
Before moving on
Keep the parser, its contract, and the boundary checks together. The final project combines this validation with CSV reading, grouped records, and careful reporting. It will also show which parts require stronger schema checks for broader real-world files.
Official sources checked October 2, 2026. Examples, exercises, and rubrics are original Noetrion teaching material; fictional datasets are identified below.