pythonbeginner45 minPython 3

CSV Ingestion Validator with Custom Exceptions

Build a robust schema and data validator in Python that categorizes bad records using a custom exception hierarchy and routes defects to a quarantine log.

The incident

Company
MediSync Health
Engineer
Rahul Varma, Data Pipeline Engineer
Trigger
Diagnostic partner labs uploaded daily blood test CSVs in inconsistent formats, causing the nightly ETL pipeline to crash on the first invalid row.
Stakes
Over 1,800 patient diagnostic reports were delayed each morning, stalling critical clinical treatments and generating 6.5 lakh rupees in SLA non-compliance penalties.

MediSync Health operated a cloud diagnostic portal aggregating laboratory blood test results from forty-five independent hospital clinics every night across major metropolitan centers. Each clinic exported daily patient test metrics in CSV format and uploaded them to an SFTP drop folder for automated overnight processing. The data engineering team ran an automated batch ingestion pipeline at two o'clock every morning to parse records, validate physiological biomarker readings, and load cleaned diagnostic records into the clinical warehouse database for immediate physician review during morning clinical rounds.

Unfortunately, partner clinic formats varied wildly across hospital software vendors and legacy medical record exports, introducing frequent structural errors and inconsistent schema mappings. Lab technicians frequently entered string text like PENDING or INVALID into numerical glucose fields, submitted negative physiological values for blood markers, or omitted essential metadata columns like patient identifier entirely. The existing ingestion script relied on bare type conversions that crashed on the first corrupted row, terminating the entire batch job before subsequent clinical files could even be inspected.

Because the ingestion pipeline failed abruptly on the first defective record, all subsequent eighteen hundred patient test reports remained completely unprocessed until engineers woke up to troubleshoot at seven in the morning. Doctors in emergency wards and outpatient clinics were left without critical pathology data for morning patient rounds, and MediSync incurred 6.5 lakh rupees in hospital SLA penalties across twelve contract hospital networks due to delayed laboratory report turnarounds and breach of clinical contracts.

Rahul Varma, a data pipeline engineer on the healthcare platform team, was charged with overhauling the ingestion engine to make it resilient against bad inputs. Instead of crashing on first error, the pipeline needed to validate schemas and row values systematically and record actionable audit traces for hospital administrators. Rahul developed a custom exception hierarchy rooted in DataValidationError, branching into MissingColumnError, TypeConversionError, and RangeError to categorize incoming defects with precision and isolate root causes.

By implementing row-level exception trapping, valid patient records flowed seamlessly into downstream clinical tables, while corrupted entries were diverted into a structured dead-letter quarantine audit table with precise error reasons and row indices. This architectural quarantine allowed healthcare providers to access uncorrupted patient data immediately without delay, while laboratory administrators received daily error reports detailing specific data entry defects that required correction at the clinic level without halting enterprise pipelines.

The overhauled ingestion utility transformed clinical data reliability across the partner hospital network. The nightly ingestion summary, valid record counts, and quarantined error audit log are submitted daily to the Clinical Data Manager and Compliance Auditor for regulatory oversight and partner laboratory SLA scoring across monthly clinical governance and contract review meetings.

What you will learn

  • Design an enterprise exception hierarchy using Python class inheritance
  • Isolate row-level validation errors without crashing batch pipelines
  • Validate schema column completeness against required field lists
  • Route malformed records into dead-letter quarantine reports

What you need first

Custom Exception HierarchiesPython context managers and exceptions

Inheriting from a shared domain base exception allows catching either specific errors or all pipeline validation errors polymorphically.

PythonMini example
class Base(Exception): pass
class Sub(Base): pass
print(issubclass(Sub, Base))
True
Watch out

Catching the base exception before derived exceptions in try/except blocks prevents specific error handlers from triggering.

Row-Level Quarantine PatternPython data structures

Collecting defective rows with row indices and diagnostic messages into a quarantine list preserves batch progress for healthy records.

PythonMini example
quarantine = []; quarantine.append((1, 'bad value')); print(len(quarantine))
1
Watch out

Halting execution on the first error prevents discovering remaining defective rows in the batch.

Numerical Range BoundsPython control flow

Checking min_val <= val <= max_val after casting prevents physiological anomalies like negative glucose from entering data warehouses.

PythonMini example
val = -10.0; valid = 20.0 <= val <= 800.0; print(valid)
False
Watch out

Performing range comparisons on uncast string inputs leads to alphabetical comparison errors like '9' > '100'.

Architecture and data flow

Problem scenario diagram for py-proj-csv-validator-exceptions
Problem scenario diagram for py-proj-csv-validator-exceptions. Figure 1: Operational problem and business context
Data flow diagram for py-proj-csv-validator-exceptions
Data flow diagram for py-proj-csv-validator-exceptions. Figure 2: Pipeline execution and data transformation flow
Result shape diagram for py-proj-csv-validator-exceptions
Result shape diagram for py-proj-csv-validator-exceptions. Figure 3: Output verification and method results

The data

A raw CSV dataset containing patient identifiers, test dates, laboratory glucose readings, and diagnostic status flags.

patient_lab_results: Raw clinic laboratory test results log

patient_idtest_dateglucose_levelstatus
P1012024-03-01110.5NORMAL
P1022024-03-01-15.0ERROR
P1032024-03-02142.0ELEVATED
P1042024-03-02PENDINGUNKNOWN
P1052024-03-0398.2NORMAL

Step by step

Read each step, run its code, then check your output against the one shown.

  1. Define custom validation exception hierarchy

    We inspect the raw diagnostic CSV input containing a mix of valid patient records and common data entry errors. The dataset contains five rows representing blood glucose measurements across multiple hospital wards. Row two contains a negative glucose value, while row four contains an un-parsed string token 'PENDING'. In medical ingestion pipelines, parsing must anticipate malformed inputs without corrupting numerical aggregates. We inspect the raw rows to identify required schema columns and establish expected validation boundaries before implementing parsing logic and exception handling.

    PythonStep 1
    class DataValidationError(Exception):
        pass
    
    class MissingColumnError(DataValidationError):
        pass
    
    class TypeConversionError(DataValidationError):
        pass
    
    class RangeError(DataValidationError):
        pass
    
    is_missing_sub = issubclass(MissingColumnError, DataValidationError)
    is_range_sub = issubclass(RangeError, DataValidationError)
    print(f"Hierarchy verified: MissingColumnError is DataValidationError: {is_missing_sub} | RangeError is DataValidationError: {is_range_sub}")
    Hierarchy verified: MissingColumnError is DataValidationError: True | RangeError is DataValidationError: True
    Common mistake

    Watch out: Inheriting custom exceptions from BaseException instead of Exception interferes with system signals like KeyboardInterrupt.

    How you will know

    The output confirms that derived exception classes are valid subclasses of DataValidationError.

  2. Demonstrate batch termination from unhandled casting

    Wrong turn first: this step shows the mistake before the fix

    We demonstrate why standard naive parsing causes batch ETL pipelines to crash in production. When an ingestion script iterates through raw rows and attempts direct float conversion without error handling, encountering an un-parsed string like 'PENDING' raises an uncaught ValueError. In a naive pipeline, this exception terminates the Python process immediately, preventing subsequent valid patient records from being processed. This all-or-nothing failure mode causes severe operational delays and breaches healthcare delivery agreements across partner clinics. Defective rows must be quarantined instead.

    PythonStep 2
    try:
        bad_val = "INVALID"
        val = float(bad_val)
    except ValueError as e:
        print("Pipeline crashed on row 2: ValueError: could not convert string to float: 'INVALID'")
    Pipeline crashed on row 2: ValueError: could not convert string to float: 'INVALID'
    Common mistake

    Watch out: Converting external strings to floats without try/except handling creates brittle single-point-of-failure pipelines.

    How you will know

    The output demonstrates how an unhandled ValueError halts batch execution prematurely.

  3. Validate header schema completeness

    Before validating individual data values in a batch, an ingestion pipeline must verify that the file contains all mandatory header columns. In medical reporting, omitting critical fields like patient_id or glucose prevents downstream systems from linking measurements to medical histories. We define a set of required field names and compare it against the incoming CSV headers using set operations. If any required column is missing, the validator raises a MissingColumnError before attempting to process data rows, preventing confusing downstream key errors.

    PythonStep 3
    required_cols = ["patient_id", "test_date", "glucose_level", "status"]
    sample_header = ["patient_id", "test_date", "glucose_level", "status"]
    
    missing = [col for col in required_cols if col not in sample_header]
    if not missing:
        print(f"Header validation passed: Required columns {required_cols} present.")
    Header validation passed: Required columns ['patient_id', 'test_date', 'glucose_level', 'status'] present.
    Common mistake

    Watch out: Checking column existence using string containment in raw text produces false positives if a column name is a substring of another.

    How you will know

    The check verifies that all 4 mandatory schema columns are present in the header.

  4. Implement row-level validation and quarantine routing

    We construct a row validation function that applies schema and business rules sequentially. The validator first verifies that all required columns are present in the row dictionary. Next, it validates that numerical fields can be converted to float without raising ValueError. Finally, it asserts that physiological readings fall within plausible biological ranges, specifically verifying that glucose levels are strictly positive. Any violation raises the corresponding custom exception.

    PythonStep 4
    import csv, io
    
    class DataValidationError(Exception): pass
    class TypeConversionError(DataValidationError): pass
    class RangeError(DataValidationError): pass
    
    raw_csv = """patient_id,test_date,glucose_level,status
    P101,2024-03-01,110.5,NORMAL
    P102,2024-03-01,-15.0,ERROR
    P103,2024-03-02,142.0,ELEVATED
    P104,2024-03-02,PENDING,UNKNOWN
    P105,2024-03-03,98.2,NORMAL
    """
    
    reader = list(csv.DictReader(io.StringIO(raw_csv.strip())))
    valid = []
    quarantine = []
    
    for idx, r in enumerate(reader, start=1):
        raw_glucose = r["glucose_level"]
        try:
            try:
                val = float(raw_glucose)
            except ValueError:
                raise TypeConversionError(f"Cannot cast '{raw_glucose}' to numeric glucose value")
            if not (20.0 <= val <= 800.0):
                raise RangeError(f"Glucose value {val} below minimum safe limit 20.0")
            valid.append(r)
        except DataValidationError as err:
            quarantine.append((idx, type(err).__name__, str(err)))
    
    print(f"Validated {len(reader)} rows: {len(valid)} valid records, {len(quarantine)} quarantined errors.")
    Validated 5 rows: 3 valid records, 2 quarantined errors.
    Common mistake

    Watch out: Discarding error diagnostic strings during quarantine prevents upstream clinic technicians from knowing what caused the rejection.

    How you will know

    The output confirms 3 valid records retained and 2 malformed records routed to quarantine.

  5. Format ingestion summary and quarantine audit log

    Finally, we implement a resilient batch ingestion loop that processes patient records with row-level error isolation. For each row in the input dataset, we execute the validator inside a try-except block catching DataValidationError. Valid records are committed to a clean records list, while defective rows are diverted to a quarantine log capturing the patient ID, row index, and specific failure reason. We print an executive summary showing throughput counts and quarantine details to provide operators with immediate visibility into data quality.

    PythonStep 5
    import csv, io
    
    class DataValidationError(Exception): pass
    class TypeConversionError(DataValidationError): pass
    class RangeError(DataValidationError): pass
    
    raw_csv = """patient_id,test_date,glucose_level,status
    P101,2024-03-01,110.5,NORMAL
    P102,2024-03-01,-15.0,ERROR
    P103,2024-03-02,142.0,ELEVATED
    P104,2024-03-02,PENDING,UNKNOWN
    P105,2024-03-03,98.2,NORMAL
    """
    
    reader = list(csv.DictReader(io.StringIO(raw_csv.strip())))
    valid = []
    quarantine = []
    
    for idx, r in enumerate(reader, start=1):
        raw_glucose = r["glucose_level"]
        try:
            try:
                val = float(raw_glucose)
            except ValueError:
                raise TypeConversionError(f"Cannot cast '{raw_glucose}' to numeric glucose value")
            if not (20.0 <= val <= 800.0):
                raise RangeError(f"Glucose value {val} below minimum safe limit 20.0")
            valid.append(r)
        except DataValidationError as err:
            quarantine.append((idx, type(err).__name__, str(err)))
    
    print("=== Ingestion Summary ===")
    print(f"Total Processed: {len(reader)}")
    print(f"Valid Records: {len(valid)} ({round(len(valid) * 100 / len(reader), 1)}%)")
    print(f"Quarantined: {len(quarantine)} ({round(len(quarantine) * 100 / len(reader), 1)}%)")
    print("Quarantine Log:")
    for row_idx, err_name, msg in quarantine:
        print(f"  Row {row_idx}: {err_name}: {msg}")
    === Ingestion Summary ===
    Total Processed: 5
    Valid Records: 3 (60.0%)
    Quarantined: 2 (40.0%)
    Quarantine Log:
      Row 2: RangeError: Glucose value -15.0 below minimum safe limit 20.0
      Row 4: TypeConversionError: Cannot cast 'PENDING' to numeric glucose value
    Common mistake

    Watch out: Omitting total processed counts obscures whether a 100% valid rate represents 1 row or 10,000 rows.

    How you will know

    The formatted summary prints 3 valid records (60.0%), 2 quarantined (40.0%), and lists both error lines.

Build it yourself

Write your solution below, run it in your browser, and pass every check.

Python 3Ready to run
Loading editor…
Press Run Code to evaluate
CSV validator executes without unhandled errors
Accurately reports valid record count and percentage
Accurately reports quarantined count and percentage
Identifies negative glucose range error

What the result means

Interpretation

Out of 5 patient records processed, 3 records (60.0%) satisfied all physiological and schema invariants, while 2 records were successfully quarantined due to a negative reading (-15.0) and an un-parsed string ('PENDING'). Rather than aborting the entire batch and withholding reports for the 3 healthy patients, the system ingested valid records immediately and generated an itemized error log for laboratory remediation.

The message you would send

Hi Clinical Operations, the overnight diagnostic ingestion job processed 5 patient lab records with a 60% clean throughput. 3 valid patient reports have been committed to the warehouse, while 2 malformed records (Row 2 negative glucose, Row 4 non-numeric text) were diverted to quarantine. The remediation log has been dispatched to partner labs.

Put it on your CV

Resume or LinkedIn bullet

OutputCopy this line
Engineered a resilient batch ingestion validator in Python with a custom domain exception hierarchy, routing malformed laboratory records to dead-letter quarantine without halting pipeline throughput.

README for GitHub

OutputREADME.md
# CSV Ingestion Validator with Custom Exceptions

## Business Context
Nightly patient diagnostic ingestion crashed abruptly upon encountering non-numeric test strings, delaying critical lab results and breaching healthcare SLAs.

## Solution Architecture
- Structured a custom exception tree rooted in `DataValidationError` with specific derived exception classes.
- Implemented row-level exception trapping to route defects into an append-only quarantine audit log without crashing the batch.
- Produced executive reconciliation summaries detailing valid throughput and itemized defect causes.

## Verification
Passes all automated verification checks on Python 3 standard library.
Stretch goal

Serialize the quarantine audit log into a structured JSON dead-letter queue payload and publish an alert message if the error rate exceeds 15%.

Keep going