How to Validate Data in a Python Pipeline

A helper module to validate common record types — email, positive integer, and non-empty string list — before processing data in a pipeline.

Easy Python 3.9+ Aug 9, 2026 Data pipelines & processing 12 views 0 copies

Python code

44 lines
Python 3.9+
from typing import Any, Iterable


def is_valid_email(email: str) -> bool:
    """Basic email check: one '@', no spaces, dot after '@'."""
    if "@" not in email or " " in email:
        return False
    local, _, domain = email.partition("@")
    return bool(local) and "." in domain


def is_positive_int(value: Any) -> bool:
    """Return True if value is a positive integer (not bool)."""
    if isinstance(value, bool):
        return False
    return isinstance(value, int) and value > 0


def is_non_empty_string_list(values: Iterable[Any]) -> bool:
    """Return True if all items are non-empty strings."""
    return all(isinstance(v, str) and v.strip() for v in values)


def clean_pipeline_record(record: dict) -> dict:
    """Validate common pipeline fields; raise ValueError on bad data."""
    if not is_positive_int(record.get("user_id")):
        raise ValueError(f"Invalid user_id: {record.get('user_id')!r}")
    if not is_valid_email(record.get("email", "")):
        raise ValueError(f"Invalid email: {record.get('email')!r}")
    if not is_non_empty_string_list(record.get("tags", [])):
        raise ValueError(f"Invalid tags: {record.get('tags')!r}")
    return record


if __name__ == "__main__":
    samples = [
        {"user_id": 42, "email": "ada@example.com", "tags": ["python", "data"]},
        {"user_id": -1, "email": "bad@example.com", "tags": ["x"]},
    ]
    for rec in samples:
        try:
            print("OK:", clean_pipeline_record(rec))
        except ValueError as exc:
            print("REJECTED:", exc)

Output

stdout
OK: {'user_id': 42, 'email': 'ada@example.com', 'tags': ['python', 'data']}
REJECTED: Invalid user_id: -1

How it works

The clean_pipeline_record function enforces field-level checks before any downstream work, raising ValueError on bad data so failures are loud and early. Each helper (is_valid_email, is_positive_int, is_non_empty_string_list) is isolated and reusable, keeping validation logic testable and composable. The isinstance(value, int) check excludes booleans because True and False are subclasses of int in Python. Splitting email with partition("@") handles strings with multiple at-signs cleanly by only splitting on the first occurrence. This pattern scales well: add new validators for other field types without touching the main pipeline logic.

Common mistakes

  • Using `bool` as a valid integer, since `isinstance(True, int)` returns True
  • Not stripping whitespace from strings before checking if they are empty
  • Assuming `partition` handles multiple '@' characters correctly (it splits only on the first)
  • Returning the original dict instead of a validated copy if downstream code mutates it

Variations

  1. Use `dataclasses` with validation in `__post_init__` for typed records
  2. Leverage Pydantic for schema validation with automatic error messages
  3. Add a generic `validate_field(field_name, value, validator)` helper to reduce repetition

Real-world use cases

  • Cleaning incoming API payloads in an ETL job before writing to a database.
  • Validating user-submitted form data in a web framework like Flask or Django.
  • Filtering malformed rows in a batch processing script before sending to a data warehouse.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Data pipelines & processing

Related tutorials and quizzes for this topic.