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.
Python code
44 linesfrom 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
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
- Use `dataclasses` with validation in `__post_init__` for typed records
- Leverage Pydantic for schema validation with automatic error messages
- 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
More from Data pipelines & processing
- Add a UUID Surrogate Key to Each Row in a CSV with Python easy
- Attach Source File Metadata to Records in Python easy
- Build a Python Utility That Detects Duplicate Records Across Multiple Excel Sheets medium
- Check Null Rate Threshold in PySpark DataFrame medium
- Count Records Processed per Category in Python easy
- Create Data Helper Functions in Python for Beginners easy
Keep learning
Related tutorials and quizzes for this topic.