Schema validation
Before any model — or any pipeline stage — touches your data, the data has to be valid. A record (a dict of fields) is valid when every field you need is present, each value is the right type, and the numbers fall in a sensible range. Catching a bad row at the door is far cheaper than debugging a corrupted training run later.
Validation breaks into three independent checks:
- Required fields — is every key you depend on actually in the dict? (A field can be present with a value of
None— present and missing are different things.) - Types — is
ageanint, isnameastr? You check withisinstance(value, expected_type). - Range — is the number between sane bounds?
low <= value <= highas a single chained comparison.
Each check is a small function returning True / False. Keeping them separate means you can test each one on its own and combine them however a given record demands.
*In production you'd reach for a library like Pydantic to declare a schema. Writing it by hand first makes the logic transparent — you can see exactly what "valid" means.*
Syntax
def check_required(record, required_fields):
for field in required_fields:
if field not in record:
return False
return True
def check_types(record, schema): # schema: {"age": int, ...}
for field, expected_type in schema.items():
if field not in record:
return False
if not isinstance(record[field], expected_type):
return False
return True
def check_range(value, low, high):
return low <= value <= high # inclusive both endsWorked examples
Required fields — present vs missing
def check_required(record, required_fields):
for field in required_fields:
if field not in record:
return False
return True
record = {"customer_id": "C001", "age": 34, "spend": 250.0}
print(check_required(record, ["customer_id", "age", "spend"])) # True
print(check_required(record, ["customer_id", "email"])) # False
# Output:
# True
# FalseType check — a type, or a tuple of allowed types
def check_types(record, schema):
for field, expected_type in schema.items():
if field not in record:
return False
if not isinstance(record[field], expected_type):
return False
return True
schema = {"age": int, "spend": (int, float)} # spend may be int OR float
print(check_types({"age": 34, "spend": 250.0}, schema)) # True
print(check_types({"age": "34", "spend": 250.0}, schema)) # False (age is str)
print(check_types({"spend": 250.0}, schema)) # False (age missing)
# Output:
# True
# False
# FalseRange check — bounds are inclusive
def check_range(value, low, high):
return low <= value <= high
print(check_range(34, 18, 120)) # True
print(check_range(18, 18, 120)) # True (low bound included)
print(check_range(17, 18, 120)) # False
# Output:
# True
# True
# FalseCommon mistakes & gotchas
Present-but-None is still present
field not in record is False when the key exists, even if its value is None. So check_required passes a record like {'age': None} — the field IS there. If you also want to reject None values, that's a *separate* check; don't fold it into the required-fields test.
bool is a subclass of int
In Python isinstance(True, int) is True — booleans are ints under the hood. So check_types({'age': True}, {'age': int}) passes. If a field must be a real number and not a bool, you have to exclude bool explicitly (e.g. type(v) is int), because isinstance won't.
Pass the type, not an instance
The schema maps to the type *object* — int, str, (int, float) — not an example value. {"age": 0} is wrong; isinstance(34, 0) raises TypeError: isinstance() arg 2 must be a type. Use the bare type name.
Why it matters
Garbage in, garbage out is the oldest law in data work. A model trained on rows where 'age' is sometimes the string "34" and sometimes 34 learns nonsense. Validation is the unglamorous gate that keeps every downstream stage — features, training, metrics — working on data it can trust.