Testing Pipeline Steps with Assertions
Add row-count checks, null assertions, and expected-column guards at each stage so errors surface immediately.
Testing Pipeline Steps with Assertions is a free Pandas & NumPy Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Pandas & NumPy Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Test Pipeline Steps?
A data pipeline that runs without errors can still produce silently wrong output: rows dropped by the wrong filter, a column multiplied by the wrong factor, or a merge that duplicates rows because the join key was not unique. The only way to catch these silent failures is to embed assertions that verify expected properties of the data at each pipeline stage. Assertions turn logical bugs into loud, immediately visible errors.
import pandas as pd
import numpy as np
# A transform that looks correct but has a bug:
def compute_revenue_buggy(df):
# BUG: unit_price should be multiplied, not added
df['revenue'] = df['quantity'] + df['unit_price']
return df
# Without assertions, this runs silently with wrong numbers.Row Count Checks
After every filtering step, assert that the resulting row count is within an expected range. Too few rows means the filter was too aggressive; too many rows means a join duplicated records. Express the expected range as a fraction of the input: for example, a null-drop step should never remove more than 30 % of rows in healthy data. This guard catches unexpected data quality changes in upstream sources.
def drop_nulls_guarded(df, required_cols, max_drop_fraction=0.3):
before = len(df)
result = df.dropna(subset=required_cols)
after = len(result)
drop_fraction = (before - after) / before
assert drop_fraction <= max_drop_fraction, \
f'dropna removed {drop_fraction:.1%} of rows (limit {max_drop_fraction:.1%})'
return resultColumn Existence Checks
Assert that all expected output columns exist after every transformation function. If a rename step accidentally uses the wrong key, the resulting column is missing — and the error only surfaces much later when another step tries to use it. An assertion immediately after the transform catches the problem at the source rather than three steps later with a confusing KeyError.
def compute_revenue(df):
df = df.assign(revenue=lambda d: d['quantity'] * d['unit_price'])
# Guard: check that the new column exists and is non-null
assert 'revenue' in df.columns, 'revenue column not created'
assert df['revenue'].notna().all(), 'revenue has unexpected NaNs'
return dfValue Range Assertions
After computing a revenue column, assert it is non-negative. After parsing dates, assert they fall within the expected year range. After encoding categories, assert no unexpected codes appear. Each assertion is a data contract that documents expected behaviour and catches violations early. Write them as assertions rather than print statements so they raise errors in automated pipeline runs.
def validate_computed_columns(df):
assert (df['revenue'] >= 0).all(), \
f'Negative revenue: {df[df["revenue"] < 0]["revenue"].head().tolist()}'
assert (df['quantity'] > 0).all(), \
'Non-positive quantity found'
assert df['revenue'].between(0, 1_000_000).all(), \
'Revenue out of plausible range'
print('Value range checks passed.')No-Duplication Guards After Merges
A common data bug is a many-to-many merge that accidentally multiplies rows. After every pd.merge(), assert that the row count is expected — typically that it did not exceed the left DataFrame's row count for a left join. Also assert that the key column is unique if it should be, so you catch accidental cross-joins immediately.
def safe_merge(left, right, on, how='left'):
before = len(left)
result = left.merge(right, on=on, how=how)
after = len(result)
if how == 'left':
assert after == before, \
f'Left join increased rows from {before} to {after} — check key uniqueness in right df'
return resultNull Assertion After Critical Steps
Certain columns must never be null at any point in the pipeline. Assert df['key_col'].notna().all() after every step that could accidentally introduce nulls — such as a merge that fails to match some rows (producing NaN in joined columns), or a map() call that returns NaN for unmapped values. Make these assertions the last two lines of every transformation function that touches those columns.
NOT_NULL_AFTER_TRANSFORM = ['order_id', 'revenue', 'category']
def post_transform_checks(df):
for col in NOT_NULL_AFTER_TRANSFORM:
null_count = df[col].isna().sum()
assert null_count == 0, \
f'{col}: {null_count} unexpected NaN values after transform'
print('Null checks passed.')
return dfBuilding a Test Suite for Pipeline Steps
Write unit tests for each pipeline function using tiny, hand-crafted DataFrames that isolate the logic being tested. Each test should: set up a minimal input, call the function, and assert the output properties. Using Python's built-in assert is sufficient for simple pipelines; for larger projects, use pytest to run all tests automatically before deploying.
def test_compute_revenue():
test_df = pd.DataFrame({
'quantity': [2, 3],
'unit_price': [10.0, 5.0]
})
result = compute_revenue(test_df)
assert 'revenue' in result.columns
assert result['revenue'].tolist() == [20.0, 15.0]
assert result['revenue'].dtype == float
print('test_compute_revenue PASSED')
test_compute_revenue()Testing Edge Cases
Good tests cover not just the happy path but also edge cases: all-null input, empty DataFrame, single-row DataFrame, and columns with extreme values. An empty DataFrame should return an empty DataFrame, not an error. A DataFrame with a single row should compute the correct result. Test these cases explicitly to ensure the pipeline handles them gracefully in production when unusual data arrives.
def test_empty_df():
empty = pd.DataFrame({'quantity': [], 'unit_price': []})
result = compute_revenue(empty)
assert len(result) == 0, 'Empty input should produce empty output'
assert 'revenue' in result.columns, 'Revenue column should still be created'
print('test_empty_df PASSED')
def test_single_row():
single = pd.DataFrame({'quantity': [1], 'unit_price': [99.0]})
result = compute_revenue(single)
assert result['revenue'].iloc[0] == 99.0
print('test_single_row PASSED')
test_empty_df()
test_single_row()Integration Test: Full Pipeline on Sample Data
Beyond unit tests on individual functions, write an integration test that runs the complete pipeline on a small representative sample of real data. Assert that the output has the expected number of columns, the key column is unique, and the total revenue is within a plausible range. This end-to-end check catches bugs in the interaction between steps that unit tests do not reveal.
def integration_test(config):
raw = extract(config)
clean = transform(raw, config)
assert set(config['required_cols']).issubset(set(clean.columns))
assert clean['order_id'].is_unique
assert (clean['revenue'] >= 0).all()
assert len(clean) > 0
print(f'Integration test PASSED. Output: {clean.shape}')
integration_test(CONFIG)Continuous Testing with pytest
As the pipeline grows, collect all tests into a tests/ directory and run them with pytest from the command line. A conftest.py file can create shared fixtures like sample DataFrames. Integrate pytest into your CI/CD pipeline so every code change automatically runs all tests before deployment. A failing test blocks the deployment, preventing broken code from reaching production.
# tests/test_transform.py — example structure
# import pytest, pandas as pd
# from pipeline.transform import compute_revenue, drop_nulls
# @pytest.fixture
# def sample_df():
# return pd.DataFrame({'quantity': [2, 3], 'unit_price': [10.0, 5.0]})
# def test_revenue(sample_df):
# result = compute_revenue(sample_df)
# assert result['revenue'].tolist() == [20.0, 15.0]
print('Run: pytest tests/ -v to execute all pipeline tests')Assertions as Living Documentation
Each assertion in the pipeline is both a guard and a piece of documentation: it states, in machine-readable form, what the data must look like at that point. When a new analyst joins the project and reads the pipeline code, the assertions tell them the data contracts without requiring a separate specification document. Treat every assertion as you would a comment — make the message descriptive enough to understand the business rule it enforces.
# These assertions document business rules as code:
assert (df['order_date'] >= pd.Timestamp('2020-01-01')).all(), \
'Company was founded 2020-01-01; no orders can predate this'
assert df['region'].isin({'North', 'South', 'East', 'West', 'Central'}).all(), \
'Only 5 sales regions are valid; new regions require config update'
print('Business rules verified as assertions.')Quick Check
Test your understanding of Data Analysis concepts from this lesson.
Lesson Recap
In this lesson you learned: embedding row count, column existence, value range, and null checks as in-pipeline assertions, writing unit tests for individual transformation functions with edge-case coverage, and running integration tests and treating assertions as living data contract documentation. Next up we explore scheduling and logging pipeline runs for automated daily execution.
Frequently asked questions
Is the “Testing Pipeline Steps with Assertions” lesson free?
Yes — the full text of “Testing Pipeline Steps with Assertions” is free to read here on the web, and the Pandas & NumPy Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Pandas & NumPy Academy course, upgrade to CoddyKit PRO.
What will I learn in “Testing Pipeline Steps with Assertions”?
Add row-count checks, null assertions, and expected-column guards at each stage so errors surface immediately. You practise Pandas & NumPy Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Pandas & NumPy Academy?
No prior experience is required. Pandas & NumPy Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Testing Pipeline Steps with Assertions” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Pandas & NumPy Academy lesson?
Yes. Every Pandas & NumPy Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Structuring Transformation Steps as Functions
- Parameterising Pipelines with Config Dicts
- Testing Pipeline Steps with Assertions
- Scheduling and Logging Pipeline Runs