⏱ 18 min readTopics chapter readerLevel · Intermediate
01 · Orientation
What You'll Master Here
Validate shape, validate fields, validate records, validate the batch, then write only safe output plus evidence.
⏱ 4 min · Topic 1 of 16
Validation is how a Python data job proves that output rows are trustworthy before they reach a file, API, database, or warehouse.
This chapter turns earlier habits into a full contract system: required fields, field rules, record rules, batch rules, rejected rows, warnings, fatal errors, exception context, manifests, and tests.
The goal is not to make code noisy. The goal is to make failure paths explicit enough that bad data is stopped, explained, and measured.
Core mental model
Validate shape, validate fields, validate records, validate the batch, then write only safe output plus evidence.
Why data engineers care
Silent validation failures become bad metrics, unsafe upserts, broken partitions, and expensive incident investigations.
rejected row
A row excluded from accepted output with structured reason and source context.
warning
A non-blocking quality signal that is reported while the row may continue.
fatal error
A batch-level problem that should stop the run, such as missing required columns.
contract evidence
Counts and reports proving what validation accepted, rejected, warned, or stopped.
schema check
columns exist
field rules
types and ranges
record rules
row is coherent
batch rules
duplicates and totals
output
safe rows + evidence
Common mistake
Catching every error and continuing without evidence. The job appears resilient while silently losing correctness.
Better habit
Separate warning, rejection, and fatal paths.
Attach file, line, field, value, and reason to failures.
Test each validation rule with tiny examples.
What to say
I would validate schema first, then field and record rules, emit rejected rows with source context, stop on fatal batch errors, and write a manifest that reconciles counts.
Remember this
Validation is not just defensive code. It is the data contract made executable.
02 · Contract
Validation Is A Data Contract
Every rule has a condition, a level, and a failure path.
⏱ 4 min · Topic 2 of 16
A validation rule is a promise about what accepted output means. If amount is required and non-negative, accepted rows must satisfy that promise every time.
Contracts should be close to the boundary they protect: schema validation before reading rows, row validation before normalization, batch validation before writing output.
The contract is incomplete unless it names the failure path.
Core mental model
Every rule has a condition, a level, and a failure path.
Why data engineers care
Downstream systems trust accepted rows because validation defines what accepted means.
Rule contract table
Rule
Level
Failure path
amount is required and decimal
field
reject row
event_time is aware UTC datetime
field
reject row
order_id unique in batch
batch
fatal or reject duplicates by policy
source file has required columns
schema
fatal error
Common mistake
Writing validation rules as comments only. The job relies on human memory instead of executable checks.
Better habit
Write rules as code and as readable tables.
Name the validation level.
Name the failure path.
Review habit
A reviewer should be able to read the validation table and predict the rejected-row output.
Remember this
A data contract is only real when the code enforces it and reports violations.
03 · Fields
Required Fields And Field-Level Rules
Each field validator proves one value belongs in the accepted contract.
⏱ 5 min · Topic 3 of 16
Field-level validation checks one value at a time: requiredness, type, range, allowed set, nullability, and parseability.
A strong field rule returns a clear reason that names the field and bad value.
Do this before normalization depends on the value, so invalid data cannot become a misleading default.
Core mental model
Each field validator proves one value belongs in the accepted contract.
Why data engineers care
Field errors are the most common bad-data path and the easiest to explain when context is preserved.
Validate required amount and statusworked example
Python
Input data
raw_orders3 rows
line_number
order_id
amount
status
2
1001
12.30
paid
3
1002
bad
paid
4
1003
7.00
mystery
fromdecimalimportDecimal,InvalidOperationALLOWED_STATUSES={"paid","refunded","pending"}defvalidate_amount(value:str)->Decimal:ifvaluein{"",None}:raiseValueError("amount is required")try:amount=Decimal(value)exceptInvalidOperationasexc:raiseValueError("amount must be decimal")fromexcifamount<0:raiseValueError("amount must be non-negative")returnamountdefvalidate_status(value:str)->str:ifvaluenotinALLOWED_STATUSES:raiseValueError("status is not allowed")returnvalue
Result · 3 rows
line_number
status
detail
2
accepted
amount Decimal("12.30"), status paid
3
rejected
amount must be decimal
4
rejected
status is not allowed
Common mistake
Defaulting invalid amount to zero. Revenue and rejection counts both become wrong.
Better habit
Validate required before parse.
Use specific reason strings.
Reject invalid values before aggregation.
Reason quality
A rejected-row reason should be specific enough for a source owner to fix the row without reading your code.
Remember this
Field validation is where raw values earn the right to become accepted data.
04 · Levels
Record-Level vs Batch-Level Validation
Validate at the smallest level that has enough information.
⏱ 5 min · Topic 4 of 16
Record-level rules inspect one row: required fields, valid amount, coherent status, or timestamp parseability.
Batch-level rules need more than one row: duplicate keys, minimum row counts, reconciliation totals, or one-current-row-per-entity.
Schema-level rules protect the whole file before row validation begins.
Core mental model
Validate at the smallest level that has enough information.
Why data engineers care
A row can be valid alone but invalid in a batch because its key is duplicated.
Detect duplicate keys at batch levelworked example
Trying to detect duplicates inside a single-row validator. The validator lacks the population needed to answer the question.
Better habit
Run schema checks first.
Run row checks before output.
Run batch checks before write/commit.
Level language
Say whether a rule is schema-level, record-level, or batch-level. It shows you know where the information lives.
Remember this
Validation level determines when and how the rule can be enforced.
05 · Schema
Schema Checks And Type Contracts
Schema validation protects the whole batch; field validation protects individual values.
⏱ 4 min · Topic 5 of 16
Schema checks make sure the source has the columns or keys required before row logic begins.
For CSV, this often means checking headers. For JSON-like records, it means checking required keys. For typed outputs, it means proving the normalized shape before writing.
A missing required column is usually fatal because every row is affected.
Core mental model
Schema validation protects the whole batch; field validation protects individual values.
Why data engineers care
Running row validation against the wrong schema creates noisy failures or misleading output.
Fail fast on missing required columnsworked example
Letting every row fail because a header is missing. The real issue is obscured by repetitive row errors.
Better habit
Validate schema before row loops.
Treat missing required columns as fatal.
Store schema version in manifests when available.
Fail-loud boundary
Bad schema is usually not a rejected-row problem. It means the batch contract itself is broken.
Remember this
Schema checks stop impossible jobs before they produce misleading row-level noise.
06 · Failure paths
Warnings vs Rejections vs Fatal Errors
Warning keeps, rejection excludes, fatal stops.
⏱ 5 min · Topic 6 of 16
Not every issue deserves the same response. A warning reports something unusual while keeping the row. A rejection excludes a bad row. A fatal error stops the batch.
The distinction should be policy, not mood. Unknown status might be fatal in finance, a rejection in order processing, or a warning during a migration.
Once chosen, the policy should be encoded and counted.
Core mental model
Warning keeps, rejection excludes, fatal stops.
Why data engineers care
Over-failing makes jobs brittle; under-failing lets bad data through.
Classify validation outcomesworked example
Python
outcomes=[{"kind":"warning","field":"coupon_code","reason":"unknown coupon kept"},{"kind":"rejected","field":"amount","reason":"amount must be decimal"},{"kind":"fatal","field":"headers","reason":"event_time column missing"},]
Result · 3 rows
kind
field
action
warning
coupon_code
keep row and report
rejected
amount
exclude row and report
fatal
headers
stop the run
Common mistake
Treating all validation issues as exceptions. The code cannot distinguish recoverable row failures from broken batches.
Better habit
Define policy per rule.
Count each lane separately.
Make warnings visible in manifests.
Policy table
A warning/reject/fatal table is often clearer than paragraphs of exception handling.
Remember this
Failure path policy is part of the data contract.
07 · Evidence
Rejected-Row Reports
Every rejected row needs source context plus a fixable reason.
⏱ 5 min · Topic 7 of 16
A rejected-row report is structured evidence for excluded records. It should be easy to join back to the source and easy for a human to understand.
The minimum useful fields are file_name, line_number, field, value, and reason. Add batch_id or source system when needed.
Rejected rows are output, not garbage. They are how the pipeline stays honest.
Core mental model
Every rejected row needs source context plus a fixable reason.
Why data engineers care
Without rejected-row reports, row loss becomes invisible.
Build a rejected-row recordworked example
Python
defreject_row(file_name,line_number,field,value,reason):return{"file_name":file_name,"line_number":line_number,"field":field,"value":value,"reason":reason,}rejected=reject_row("orders.csv",3,"amount","bad","amount must be decimal")
Result · 1 row
file_name
line_number
field
value
reason
orders.csv
3
amount
bad
amount must be decimal
Common mistake
Storing only the reason and not source context. Nobody can locate the bad row in the original file.
Better habit
Keep raw value when safe.
Include source location.
Use consistent reason strings.
Data ownership
Rejected-row reports are often the artifact shared with source owners to repair upstream data.
Remember this
Rejected rows are a first-class output of trustworthy data jobs.
08 · Exceptions
Exception Strategy For Pipeline Code
Raise locally, catch at the boundary that can add context, report or re-raise by policy.
⏱ 5 min · Topic 8 of 16
Exceptions are useful for impossible states and fatal boundaries, but row-level validation should often become structured rejected output.
A good strategy is to let small validators raise specific errors, catch them at the row boundary, and convert them into rejected rows with context.
When re-raising, use exception chaining so the low-level cause is not lost.
Core mental model
Raise locally, catch at the boundary that can add context, report or re-raise by policy.
Why data engineers care
Poor exception handling either hides useful causes or crashes jobs for recoverable row-level issues.
Wrap exceptions with row contextworked example
Python
classValidationError(Exception):passdefvalidate_order(row):try:amount=validate_amount(row["amount"])exceptValueErrorasexc:raiseValidationError(f'line {row["line_number"]} field amount: {exc}')fromexcreturnamount
Result · 2 rows
layer
message
low-level
amount must be decimal
validation boundary
line 3 field amount: amount must be decimal
Common mistake
Bare except: pass. The job drops both data and the reason it failed.
Better habit
Catch specific exceptions.
Add source context at boundaries.
Use `raise ... from exc` when preserving cause matters.
Python exceptions
Exception chaining keeps the original cause available while adding pipeline-specific context.
Remember this
Exceptions should either become structured evidence or preserve enough context to debug the fatal failure.
09 · Exceptions
Raising, Re-Raising & Exception Chaining
Translate with `from exc`. Re-raise unchanged with a bare `raise`. Never construct a new error that hides the old one.
⏱ 6 min · Topic 9 of 16
When a low-level error crosses a boundary, you usually want to re-raise it as something the caller understands — a `ValueError` about a bad amount rather than whatever the parser threw. The question is what happens to the original.
`raise NewError(...) from exc` keeps it, as `__cause__`, and the traceback prints both with "The above exception was the direct cause of the following exception". A bare `raise NewError(...)` inside an `except` block keeps it too, as `__context__` — but nothing marks it as intentional, and the traceback says "During handling of the above exception, another exception occurred", which reads like a bug in your handler.
The third form, a bare `raise` with no argument, re-raises the current exception unchanged with its original traceback. That is the right move when you only wanted to log or clean up and the caller should still see exactly what happened.
Core mental model
Translate with `from exc`. Re-raise unchanged with a bare `raise`. Never construct a new error that hides the old one.
Why data engineers care
The stack trace is the only evidence you get at 3am. A re-raise that drops the cause turns "the amount field had a comma in it" into "something went wrong in the loader".
__cause__
The exception named by `raise ... from exc`. States that the translation was deliberate.
__context__
Whatever was being handled when this was raised. Set automatically, and reads as accidental.
bare raise
`raise` with no argument, inside `except`: re-raises the current exception with its original traceback.
The caller gets a message in its own vocabulary, and the original is attached as `__cause__` so the traceback shows both — the "what" and the "why", in one place.
Without `from`, the link is implicitworked example
Python
defparse_bare(raw):try:returnint(raw)exceptValueError:raiseValueError(f"bad amount {raw!r}")# no "from"try:parse_bare("12,5")exceptValueErrorasexc:print("cause:",exc.__cause__,"| context:",type(exc.__context__).__name__)
Result · 1 row
output
cause: None | context: ValueError
`__cause__` is `None`, so nothing says the translation was deliberate. The original survives as `__context__` and the traceback frames it as a second failure during handling — which sends the reader looking for a bug in the handler.
Which re-raise
You want to
Write
Caller sees
Log or clean up, then let it through
`raise`
The original, with its original traceback
Translate to your vocabulary
`raise MyError(...) from exc`
Yours, with the original as the stated cause
Deliberately drop a noisy cause
`raise MyError(...) from None`
Yours, with no chain - use rarely, and say why
Add context to the same error
`exc.add_note(...)` then `raise`
The original, with your note attached
Common mistake
Catching, logging and then raising a brand-new exception without `from`. The traceback reads as a failure inside your error handler. The real cause is present but framed as an accident, and everybody debugging starts in the wrong place.
Writing `raise exc` instead of a bare `raise`. It re-raises with a traceback starting here, so the frames between the origin and this line are lost.
Catching `Exception` to add context, then re-raising. You have widened the net to include programming errors and labelled them all as data problems.
Better habit
`from exc` on every translated error, without exception.
A bare `raise` when you only wanted to observe.
Catch the narrowest exception type that can actually occur there.
Interview note
"What does `raise ... from ...` do?" is a standard question and the good answer is about the reader: it records that the translation was intentional, so a traceback shows the cause rather than implying a bug in the handler.
Production note
A rejected row belongs in the rejection report, not in an exception. Reserve raising for the cases where the run genuinely cannot continue — and then make the traceback worth reading.
Remember this
Translate errors with `from exc` so the cause is recorded as deliberate, re-raise unchanged with a bare `raise`, and never construct a replacement that hides what actually happened.
10 · Context
Error Context: File, Line, Field, Value, Reason
A useful error identifies where, what, and why.
⏱ 4 min · Topic 10 of 16
A validation error without context is a puzzle. A validation error with file, line, field, value, and reason is a repair instruction.
Attach context as early as possible. Chapter 4 preserved line numbers during ingestion; this chapter uses them in validation evidence.
Be careful with sensitive values. Sometimes the report should redact raw value while preserving enough detail to debug.
Core mental model
A useful error identifies where, what, and why.
Why data engineers care
Context shortens incident resolution and lets source owners fix data without guessing.
Context-rich validation outputworked example
Python
error_context={"file_name":"orders.csv","line_number":3,"field":"amount","value":"bad","reason":"amount must be decimal",}
Result · 5 rows
field
value
file_name
orders.csv
line_number
3
field
amount
value
bad
reason
amount must be decimal
Common mistake
Reporting "invalid row" with no field or value. Debugging requires rerunning or manually inspecting the source file.
Better habit
Add file and line at ingestion.
Add field and reason at validation.
Redact sensitive values by policy.
Repairable errors
A good reason is written for the person fixing the source row, not just for the developer.
Remember this
Context turns validation errors into actionable evidence.
11 · Evidence
Contract Evidence And Run Manifests
A manifest is the receipt for validation and output.
⏱ 5 min · Topic 11 of 16
A manifest is the run-level proof that validation did what it promised.
It should include rows_seen, accepted, rejected, warning_count, fatal_error, schema_version, and output locations when available.
The manifest should reconcile with the accepted and rejected outputs.
Core mental model
A manifest is the receipt for validation and output.
Why data engineers care
Operators need one place to see whether a run was healthy and what changed.
Writing output files without a run summary. There is no quick way to know whether rejection volume spiked.
Better habit
Make counts reconcile.
Include schema version.
Include fatal error status even on failure when possible.
Incident clue
A sudden jump in rejected rows is often the first visible sign that an upstream contract changed.
Remember this
Manifest evidence makes validation observable.
12 · Quality
Data Quality Checks Before Output
Accepted rows still need output-level checks before commit.
⏱ 5 min · Topic 12 of 16
After row validation, run final quality checks before writing: duplicate keys, required non-null output columns, row count expectations, and reference coverage.
These checks protect the boundary between validated Python records and durable output.
The correct action depends on the contract. Duplicate primary keys may be fatal; optional warning counts may be reported.
Core mental model
Accepted rows still need output-level checks before commit.
Why data engineers care
A row can pass field validation while the final output set violates table-level rules.
Check duplicate accepted keys before writingworked example
Validating rows individually but never validating final output keys. A downstream table can receive duplicate business keys.
Better habit
Check output uniqueness.
Check required output fields.
Check row counts against manifests.
Senior signal
Mention final output checks after row validation; it shows you understand table-level contracts.
Remember this
Validation continues until the output contract is safe to write.
13 · Tests
Testing Validation Rules
Every validation rule deserves at least one passing case and one failing case.
⏱ 5 min · Topic 13 of 16
Validation rules are perfect for small tests because every rule has tiny input and expected output.
Test the happy path, missing required values, bad types, invalid status, duplicate keys, warning-only cases, and fatal schema errors.
Expected rejected rows should match exact field, value, and reason text.
Core mental model
Every validation rule deserves at least one passing case and one failing case.
Why data engineers care
Validation changes are high-risk because they decide which data is allowed to land.
Test a rejected-row reasonworked example
Python
deftest_rejects_bad_amount():row={"file_name":"orders.csv","line_number":3,"order_id":"1002","amount":"bad"}rejected=validate_row(row)assertrejected=={"file_name":"orders.csv","line_number":3,"field":"amount","value":"bad","reason":"amount must be decimal",}
Result · 3 rows
field
expected
field
amount
value
bad
reason
amount must be decimal
Common mistake
Testing only that an exception happened. The reason, field, and context can regress without the test noticing.
Better habit
Assert exact rejected-row shape.
Test warning and fatal paths.
Use tiny input examples.
Golden evidence
Validation tests should read like miniature incident reports: given this bad row, expect this exact evidence.
Remember this
Validation tests protect the contract and the quality of your failure evidence.
14 · Checklist
Interview And Production Checklist
Schema, field, record, batch, evidence, tests.
⏱ 4 min · Topic 14 of 16
A strong validation design states schema checks, field checks, record checks, batch checks, failure paths, rejected reports, and manifest evidence.
Use clear language: warnings are kept and reported, rejected rows are excluded and reported, fatal errors stop the run.
This is the intermediate capstone because every later boundary, APIs, databases, concurrency, orchestration, depends on trustworthy validation.
Core mental model
Schema, field, record, batch, evidence, tests.
Why data engineers care
Validation is the gate between raw uncertainty and durable data.
Ending validation discussion at "I would raise an error." The design misses rejected outputs, warning policy, and run evidence.
Better habit
Name failure paths explicitly.
Make evidence structured.
Test both accepted and rejected outcomes.
What to say
I would validate schema first, then field and batch rules, capture rejected rows with file/line/field/value/reason, stop on fatal schema failures, and write a manifest that reconciles the run.
Remember this
Trustworthy Python data jobs make bad data visible and safe data provable.
15 · Practice
Practice Lab
Predict what will go wrong before you run it, then check whether you were right about which line caused it.
⏱ 6 min · Topic 15 of 16
Four exercises for this chapter, in the module workspace. Each one runs against graded cases, and each has a trap that is in the data rather than in the algorithm.
Four exercises where failure is data rather than an exception, and the report is the deliverable.
Do them with the chapter closed. If one goes wrong, come back to the section it belongs to rather than re-reading the whole thing.
Core mental model
Predict what will go wrong before you run it, then check whether you were right about which line caused it.
Why data engineers care
Reading about a failure mode and meeting one are different memories. The second is the one that is still there under interview pressure.
Common mistake
Opening the reference solution before your own version runs. You learn what correct looks like without learning what wrong feels like, and the wrong version is the one you will write first under pressure.
Better habit
Write the contract of the function — what goes in, what comes out, what happens to a bad row — before the first line.
Run the empty-input case early. It is the one most solutions forget and the one every grader checks.
Study tip
Run each solution twice in the same process on two different inputs. Anything that behaves differently the second time is carrying state it should not.
Remember this
The chapter tells you what to watch for. These four are where you find out whether you would have.
You have completed the intermediate Python data engineering arc: transforms, streams, batch patterns, serialization, validation, and contract evidence.
⏱ 3 min · Topic 16 of 16
Next chapter
APIs, Databases & External Boundaries
You have completed the intermediate Python data engineering arc: transforms, streams, batch patterns, serialization, validation, and contract evidence.
The next chapter moves into advanced boundaries: APIs, pagination, retries, databases, secrets, and idempotent source reads.