NOETRION

Choose a topic

LESSON 8 OF 9 · Python & data foundations
DATA WORKFLOW · 4 MIN READ

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.

What you will learn
  • 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.

Text
Strip whitespace
Presence
Empty?
Type
Integer?
Range
0–100?
Each validation stage answers a different question. Passing conversion does not establish an acceptable range.

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.

InputExpected statusReason for the test
"0"Accepted as 0Valid lower boundary
"100"Accepted as 100Valid upper boundary
"-1"RejectedBelow allowed range
"101"RejectedAbove allowed range
"82.5"RejectedContract 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.

Sources and further reading

Official sources checked October 2, 2026. Examples, exercises, and rubrics are original Noetrion teaching material; fictional datasets are identified below.

Check your understanding

Why catch ValueError around int(text) rather than catch every exception and return 0?

Choose one answer