Passing Data Between Pipeline Steps
Complete the full lesson to earn 25 points
Work through each section, then tap “Mark as Complete” on the last one.
✦ Skip the page breaks and see fewer ads — read each lesson on a single page with Pro
Lesson: Mastering Data Orchestration in Machine Learning Pipelines
Introduction: The Backbone of Reproducible Machine Learning
In the modern machine learning lifecycle, the process of building a model is rarely a single, monolithic script. Instead, it is a sophisticated assembly line of discrete operations—data ingestion, cleaning, feature engineering, model training, and evaluation. We call this an ML pipeline. At the heart of any effective pipeline is the mechanism for passing data between these distinct steps. If you cannot effectively move information from a data-processing step to a training step, your pipeline collapses.
Why is this so important? When we build pipelines, we aim for modularity and reproducibility. By decoupling tasks into independent steps, we gain the ability to cache results, run steps in parallel, and swap out individual components without rebuilding the entire system. However, this modularity introduces a significant challenge: how do we ensure that the output of one step is correctly formatted, stored, and accessible to the next?
Passing data between pipeline steps is not just about moving files; it is about managing state, ensuring data lineage, and maintaining performance. If you pass data inefficiently—for instance, by serializing massive datasets into memory or writing unnecessary intermediate files to a slow disk—you will experience significant bottlenecks. Conversely, if you lack a robust mechanism for tracking what data produced which model, you lose the ability to debug your experiments. This lesson will guide you through the architectures, strategies, and best practices for managing data flow in production-grade pipelines.
Understanding Data Flow Patterns
There are three primary ways data moves through a pipeline. Understanding the trade-offs of each is essential for choosing the right architecture for your specific project.
1. File-Based Passing (The Standard Approach)
The most common approach in systems like Kubeflow, Airflow, or Argo is to use a persistent storage layer. Step A writes its output to a shared storage location (like an S3 bucket, a mounted volume, or a database), and Step B reads from that location. This is highly reliable because it creates a clear audit trail.
2. In-Memory Passing (The Low-Latency Approach)
In scenarios where tasks are tightly coupled and performance is critical, data can be passed via shared memory or specialized message queues. While this is faster, it is significantly harder to debug and recover if a process fails, as the state is transient and not easily persisted.
3. Metadata-Driven Passing (The Orchestration Approach)
Modern pipeline frameworks often separate the data from the metadata. The pipeline orchestrator passes a reference (a URI or a key) to the data rather than the data itself. The receiving step then uses this reference to fetch the necessary information. This is the gold standard for large-scale systems.
Callout: Data References vs. Data Payloads A common mistake is treating the data itself as the message. Instead, always pass references. Think of this like a library system: you do not carry the entire book to the librarian to check it out; you simply provide the ISBN. In a pipeline, passing a path or an ID allows your system to track the data's location without straining the orchestration layer with large blobs of information.
Implementing Data Passing: A Practical Workflow
Let’s look at how to implement this using a common pattern seen in Python-based orchestration frameworks. We will simulate a pipeline consisting of a Preprocessing step and a Training step.
Step 1: Defining the Data Contract
Before writing code, you must define the schema. If Step A produces a CSV file and Step B expects a JSON object, your pipeline will fail. Always define a contract—a shared understanding of the data structure.
Step 2: Implementing the Producer (Step A)
The producer must be responsible for saving its output in a predictable location.
import pandas as pd
import os
def preprocessing_step(input_path, output_directory):
# Load data
df = pd.read_csv(input_path)
# Perform transformations
processed_df = df.dropna().apply(lambda x: x * 2)
# Define output path
output_file = os.path.join(output_directory, "processed_data.parquet")
# Save the data in a standard format
processed_df.to_parquet(output_file)
# Return the reference to the next step
return output_file
Step 3: Implementing the Consumer (Step B)
The consumer should not need to know where the data came from, only where it is currently located.
def training_step(data_path):
# Load the data using the provided reference
data = pd.read_parquet(data_path)
# Train model (simplified)
print(f"Training on data from: {data_path}")
print(f"Data shape: {data.shape}")
# Output model artifact
model_path = "model.pkl"
# ... save model code here ...
return model_path
Best Practices for Data Handling
When implementing these patterns, several best practices can save you from late-night debugging sessions.
Use Standardized Serialization Formats
Avoid custom binary formats or proprietary structures. Stick to formats that are widely supported by the data science ecosystem.
- Parquet/Feather: Excellent for tabular data, providing compression and schema support.
- JSON/YAML: Great for configuration parameters or small metadata dictionaries.
- Pickle/Joblib: Use these only for Python objects (like Scikit-Learn models), but be aware of security risks and versioning issues.
Implement Idempotency
An idempotent pipeline step is one that produces the same output every time it is given the same input, regardless of how many times it is run. If your Preprocessing step crashes halfway through, you should be able to restart it without worrying about duplicate files or corrupted output. Always check if the output file already exists before writing, or use atomic "write-then-rename" operations to ensure the file is only available once fully written.
Versioning Your Artifacts
Never overwrite a file named latest_data.csv. Instead, use versioning schemes based on timestamps or unique run IDs. This allows you to track the provenance of your data. If your model performance drops, you need to be able to look back at the exact data snapshot that produced the previous, better-performing model.
Note: Always include a metadata file alongside your data. This file should contain the source version, the timestamp of creation, and the parameters used to generate the data. This makes reproducibility significantly easier when you revisit an experiment six months later.
Handling Large Datasets: Beyond Local Files
When your data exceeds the capacity of a single machine's local disk, passing data becomes a distributed systems problem. In these cases, you should move away from physical file passing and toward data pointers.
The "Data Lake" Pattern
In this pattern, all pipeline steps point to a centralized data lake (e.g., S3, Google Cloud Storage, or Azure Blob Storage). Step A writes to a bucket; Step B reads from that same bucket. This decouples the compute resources from the storage resources.
Using Dataframes in Memory (Dask/Ray)
If you are using libraries like Dask or Ray, you can pass "futures" or "proxies" between tasks. This allows the framework to manage the data distribution across a cluster automatically. You are not passing the data; you are passing a handle that the cluster uses to locate the data in the distributed memory space.
| Method | Best For | Complexity | Performance |
|---|---|---|---|
| Local Files | Small/Medium datasets, local dev | Low | Moderate |
| Shared Storage | Cloud-native, distributed pipelines | Medium | High |
| In-Memory Objects | Real-time, low latency | High | Very High |
| Database/Warehouse | Structured data, SQL workflows | High | Moderate |
Common Pitfalls and How to Avoid Them
Even with the best intentions, developers often fall into traps that make their pipelines brittle. Here are the most common issues and how to steer clear of them.
1. Hardcoding Paths
Hardcoding paths like /home/user/data/input.csv is a recipe for disaster. It ensures your code will only work on your specific machine. Always use configuration files or environment variables to inject paths into your pipeline steps.
2. Tight Coupling
If your Training step needs to know the internal logic of the Preprocessing step, they are too tightly coupled. The Training step should only care about the final output of the Preprocessing step. If you find yourself passing internal variables or intermediate objects, rethink your pipeline architecture to ensure that the interface between steps is clean and minimal.
3. Ignoring Resource Constraints
Passing a 50GB dataset into memory on a machine with 16GB of RAM will cause an immediate crash. Always consider the memory footprint of your data structures. If you are using Pandas, consider switching to Polars or using chunking (processing the file in smaller pieces) when passing data between steps.
Warning: Be extremely careful when using
picklefor data transfer. It is not secure against erroneous or maliciously constructed data. If you are passing data between different environments or services, prioritize formats like Parquet, Avro, or Protobuf.
4. Lack of Validation
Never assume the data received by a step is correct. Even if your pipeline is linear, data drift or a bug in a previous step can lead to unexpected inputs. Implement lightweight validation checks (e.g., checking for null values, verifying column names, or checking data ranges) at the start of every step.
Step-by-Step: Building a Robust Data-Passing Wrapper
To professionalize your data passing, you should wrap your steps in a function that handles the boilerplate code for you. This is essentially what pipeline frameworks like Kubeflow do under the hood.
The "Context" Wrapper Pattern
Instead of writing raw file paths, create a context object that manages the input and output directories for each step.
class PipelineContext:
def __init__(self, run_id):
self.run_id = run_id
self.base_dir = f"/data/runs/{run_id}"
def get_input(self, step_name):
return os.path.join(self.base_dir, step_name, "output.parquet")
def get_output(self, step_name):
path = os.path.join(self.base_dir, step_name)
os.makedirs(path, exist_ok=True)
return os.path.join(path, "output.parquet")
# Usage
ctx = PipelineContext(run_id="2023-10-27-001")
def step_one(ctx):
output_path = ctx.get_output("step_one")
# ... process ...
return output_path
def step_two(ctx):
input_path = ctx.get_input("step_one")
# ... load ...
return
This pattern ensures that all steps follow the same directory structure, making it trivial to inspect the artifacts of any specific pipeline run.
Managing Metadata and Provenance
Passing data is only half the battle; the other half is knowing what that data is. As your pipeline grows, you will inevitably have hundreds of files scattered across your storage. Without a metadata store, you will be unable to answer questions like:
- "Which version of the raw data was used for this model?"
- "What parameters were used in the feature engineering step?"
- "Did the data change since the last training run?"
Implementing a Simple Metadata Store
You don't need a complex system to start. A simple JSON file stored alongside your output artifacts is often enough.
{
"run_id": "exp-123",
"step_name": "feature_engineering",
"input_data_hash": "a1b2c3d4e5",
"parameters": {"scaling": "min-max", "impute": "mean"},
"output_file": "processed.parquet",
"timestamp": "2023-10-27T10:00:00Z"
}
By hashing your input data, you can quickly identify if the input has changed. If the input_data_hash in the metadata matches the current data, you can skip the step entirely (caching).
Scaling to Production: Using Orchestration Frameworks
While building your own wrapper is excellent for understanding the concepts, production systems eventually require robust orchestration frameworks. These tools handle the complexities of data passing, retries, and logging.
Popular Frameworks
- Kubeflow Pipelines: Built on Kubernetes, it provides a highly scalable way to define and run pipelines as containers. It handles data passing between containers using "Artifacts."
- Apache Airflow: Uses a "XCom" (Cross-Communication) system to pass small messages between tasks. For large data, it relies on external storage.
- Metaflow: Designed specifically for data scientists, it makes passing objects between steps as simple as using class attributes. It handles the serialization and S3 storage automatically.
When you transition to these frameworks, you will find that the core principles we discussed—idempotency, standardized storage, and metadata tracking—remain exactly the same. The framework simply automates the implementation of these patterns.
Advanced Considerations: Dealing with Schema Evolution
What happens when your upstream data source changes its schema? For example, a column that was previously an integer is now a string. This is a common cause of pipeline failures in production.
Strategies for Schema Stability
- Contract Testing: Use tools like Great Expectations to define "expectations" for your data. If the data arriving from Step A doesn't meet the expectations of Step B, the pipeline should fail immediately with a clear error message.
- Schema Enforcement: When writing to formats like Parquet or Avro, explicitly define the schema. This forces the producer to respect the data types and prevents silent failures downstream.
- Versioning Data: Treat your data like code. If the schema changes, treat it as a new version of the dataset. This allows your downstream consumers to upgrade to the new schema at their own pace.
Callout: The "Data-as-Code" Philosophy Treat your data pipelines with the same rigor you apply to your software application code. This means using version control (Git) for your pipeline definitions, implementing unit tests for your data transformation functions, and conducting code reviews on your pipeline orchestrations.
Integration Testing for Pipelines
One of the biggest mistakes teams make is skipping integration testing. They test the training script, and they test the preprocessing script, but they never test the interaction between them.
How to Build an Integration Test
An integration test should run your entire pipeline (or a subset of it) using a small, representative sample of data.
- Mocking: Use small, synthetic datasets that trigger all the logic paths in your pipeline.
- Automation: Run these tests in your CI/CD pipeline every time you commit a change to the codebase.
- Verification: Ensure the final output is not just generated, but that it matches the expected statistical properties (e.g., the mean of the output is within a reasonable range).
If your integration test fails, you know immediately that you broke the contract between your pipeline steps, saving you from deploying a faulty model to your staging or production environment.
Summary: Key Takeaways for Success
Passing data between pipeline steps is the hidden complexity that defines the reliability of your machine learning system. By following these principles, you move from "scripts that happen to run" to "production-grade pipelines."
- Decouple and Reference: Never pass large data payloads directly. Pass references (URIs or IDs) to data stored in a reliable, persistent location like an object store.
- Standardize Formats: Use widely accepted, efficient formats like Parquet for your data artifacts. This ensures compatibility and performance across different steps and frameworks.
- Prioritize Idempotency: Ensure that every step in your pipeline can be re-run safely. This is the most effective way to recover from failures without manual intervention.
- Manage Metadata: Always track the provenance of your data. A simple metadata file alongside your artifacts can save hours of debugging time by providing a record of what was done and when.
- Test the Contract: Implement schema validation and integration tests. Your pipeline is only as strong as its weakest interface; verify that the output of one step is exactly what the next step expects.
- Avoid Tight Coupling: Keep your pipeline steps independent. A step should function based on its input, regardless of how that input was generated.
- Think in Systems: As you scale, move from manual file management to orchestration frameworks that handle the heavy lifting of state management, retries, and data persistence for you.
By focusing on these core concepts, you will build pipelines that are not only efficient and scalable but also maintainable and transparent. Whether you are working on a small prototype or a massive production system, the discipline of clear data orchestration will remain a foundational skill in your career as a machine learning engineer.
Frequently Asked Questions (FAQ)
Q: Should I use a database to pass data between steps? A: Databases are great for structured data, but they can become a bottleneck if you are passing large amounts of raw, unstructured data. For ML pipelines, object storage (S3/GCS) is generally preferred for large artifacts, while databases are better for tracking metadata or small configuration parameters.
Q: How do I handle secrets like API keys when passing data? A: Never pass sensitive information as part of your data pipeline artifacts. Use secret management services (like HashiCorp Vault or AWS Secrets Manager) and inject them as environment variables into the container running your step.
Q: What is the biggest mistake beginners make in pipeline design?
A: The most common mistake is writing all the logic in one giant script. This makes it impossible to debug, cache, or scale. Even if you are a beginner, force yourself to split your code into at least three distinct functions: extract, transform, and load. This is the first step toward building a true pipeline.
Q: Can I pass Pandas dataframes between steps in memory? A: Yes, if the steps are running in the same process. However, this is rarely possible in production pipelines where steps are often containerized and run on different machines. Stick to serialization (e.g., Parquet) to ensure your pipeline is portable and scalable.
Q: How often should I run my integration tests? A: Ideally, every time you push code to your version control system. If your pipeline takes a long time to run, create a "smoke test" version that uses a very small subset of data, and run the full pipeline test nightly or before a production deployment.
Continue the course
Enjoying the courses?
Everything stays free. Pro shows fewer ads, doubles your daily points limit so you progress twice as fast, and lets you read each lesson on one page.
- ✓ Fewer advertisements
- ✓ 2× daily points limit
- ✓ Distraction-free lessons