Skip to main content

Observability-first DataFrame pipeline engine with logging, caching, and intelligent diff tracking.

Project description

dfflow

PyPI version Python Versions CI Tests License: MIT

Observability-first DataFrame pipeline engine — with step caching, conditional branching, intelligent diff tracking, and structured logging.


Table of Contents


Why dfflow?

In production ML and data engineering pipelines, DataFrames transform silently at every step. Standard Python loggers have no concept of rows, nulls, or memory — so issues like "we lost 40% of rows in step 3" go completely unnoticed until downstream.

dfflow solves this by making every transformation observable by default.

Pain Point dfflow Solution
Silent data loss between steps Shape and row diff at every step
Unknown null propagation Null diff tracking (added / removed)
Unexplained memory spikes Memory delta in MB per step
Redundant computation on unchanged data Content-hash-based step caching
No pipeline audit trail Unique run_id per execution
Blind to column schema drift Column add/remove diff
Hard to gauge transformation risk Automated LOW / MEDIUM / HIGH impact scoring
Repetitive step-tracking boilerplate @step decorator with metadata injection
Different logic for different data shapes Conditional branching with add_branch()

Architecture Overview

flowchart TD

    A[Input DataFrame] --> B[FlowPipeline]
    
    B --> C[Step Execution]
    C --> D[Branch Condition]
    
    D -->|True| E[True Path]
    D -->|False| F[False Path]
    
    E --> G[Next Step]
    F --> G
    
    G --> H[Logging + Observability]
    H --> I[Final DataFrame]

Installation

pip install dfflow

Requirements:

Requirement Version
Python >= 3.10
pandas >= 1.5

Install with dev extras (pytest, coverage, mypy, ruff):

pip install "dfflow[dev]"

Quick Start

import pandas as pd
from dfflow import FlowPipeline, Step, DFLogger, drop_nulls, lowercase_columns

df = pd.DataFrame({
    "Name":    ["Alice", None, "Bob"],
    "AGE":     [25, 30, None],
    "Revenue": [500.0, 1200.0, 800.0],
})

logger = DFLogger(log_file="pipeline.log", min_level="INFO")

pipeline = FlowPipeline(logger=logger)
pipeline.add_step(drop_nulls)
pipeline.add_step(lowercase_columns)
pipeline.add_branch(
    name="Revenue Router",
    # condition must return Python bool or numpy.bool_
    condition=lambda df: df["revenue"].mean() > 1000,
    if_true=[Step("Premium", lambda df: df.assign(tier="premium"))],
    if_false=[Step("Standard", lambda df: df.assign(tier="standard"))],
)

print(pipeline.visualize())
result = pipeline.run(df, show_observability=True)
print(result)

Core API Reference


FlowPipeline

Module: dfflow.core.pipeline Import: from dfflow import FlowPipeline

The central execution engine. Manages an ordered list of Step and Branch nodes, handles caching, logging, and observability reporting for every transformation.

Constructor

FlowPipeline(logger: DFLogger | None = None)
Parameter Type Default Description
logger DFLogger | None None Optional logger instance.

Internally assigns a unique run_id (UUID4) to each pipeline instance for audit tracing.


add_step

pipeline.add_step(step_or_func: Step | Callable) -> None

Adds a transformation step to the pipeline. Accepts either a Step instance or a function decorated with @step.

Raises TypeError if the argument is neither a Step nor a decorated callable.

from dfflow import FlowPipeline, Step, step

pipeline = FlowPipeline()
pipeline.add_step(Step("Normalize", lambda df: df / df.max()))

@step("Clean Names", cacheable=True)
def clean_names(df):
    df = df.copy()
    df["name"] = df["name"].str.strip()
    return df

pipeline.add_step(clean_names)

add_branch

pipeline.add_branch(
    name: str,
    condition: Callable[[pd.DataFrame], bool],
    if_true: list[Step],
    if_false: list[Step] | None = None,
) -> None

Adds a conditional branch node. At runtime condition(df) is evaluated — True runs if_true, False runs if_false.

Parameter Type Default Description
name str required Label shown in logs and visualize().
condition Callable[[pd.DataFrame], bool] required Predicate returning bool.
if_true list[Step] required Steps when condition is True.
if_false list[Step] | None None Steps when condition is False. None or [] = pass-through.
pipeline.add_branch(
    name="Null Guard",
    condition=lambda df: df.isnull().any().any(),
    if_true=[Step("Fill Zeros", lambda df: df.fillna(0))],
    if_false=[],
)

run

pipeline.run(df: pd.DataFrame, show_observability: bool = False) -> pd.DataFrame

Executes all nodes sequentially.

Parameter Type Default Description
df pd.DataFrame required Input DataFrame.
show_observability bool False If True, logs full diff report after every step.

Raises: TypeError if input is not a DataFrame. PipelineStepError if any step fails.


visualize

pipeline.visualize() -> str

Returns an ASCII diagram of the full pipeline including branch forks.

Pipeline Flow:

Input
  ↓
  Drop Nulls
  ↓
  ┌─ Branch: Revenue Router
  │  [TRUE]
  │    → Premium
  │  [FALSE]
  │    → Standard
  └─ merge
  ↓
Output

Step Caching

When cacheable=True, the pipeline computes an MD5 hash of the input DataFrame. If the hash matches the previous run, the step is skipped entirely.

pipeline.run(df)  # Executes
pipeline.run(df)  # Cache hit — skipped
pipeline.run(df.assign(a=[4, 5, 6]))  # Data changed — executes again

Step

Module: dfflow.core.step Import: from dfflow import Step

Step(
    name: str,
    func: Callable[..., pd.DataFrame],
    config: dict[str, Any] | None = None,
    cacheable: bool = False,
)
Parameter Type Default Description
name str required Human-readable step name.
func Callable required Transformation function — takes DataFrame, returns DataFrame.
config dict | None None Optional kwargs forwarded as func(df, **config).
cacheable bool False Enable content-hash-based caching.
from dfflow import Step

step = Step(
    name="Drop Low Values",
    func=lambda df, threshold=0: df[df["value"] > threshold],
    config={"threshold": 10},
    cacheable=True,
)

Branch

Module: dfflow.core.branch Import: from dfflow import Branch

Branch(
    name: str,
    condition: Callable[[pd.DataFrame], bool],
    if_true: list[Step],
    if_false: list[Step] = [],
)
Parameter Type Default Description
name str required Label shown in logs and visualize().
condition Callable[[pd.DataFrame], bool] required Predicate returning bool.
if_true list[Step] required Steps when condition is True.
if_false list[Step] [] Steps when condition is False. Empty = pass-through.

Raises:

  • ValueError — empty name, or both paths empty
  • TypeError — condition not callable, or paths contain non-Step objects

In most cases use pipeline.add_branch() instead of instantiating Branch directly.


DFLogger

Module: dfflow.logging.logger Import: from dfflow import DFLogger

DFLogger(
    log_file: str = "dfflow.log",
    min_level: str = "INFO",
    max_rows: int | None = 20,
    max_cols: int | None = 20,
    mode: str = "text",
    file_mode: str = "w"
)
Parameter Type Default Description
log_file str "dfflow.log" Output file path.
min_level str "INFO" Minimum level. One of: DEBUG, INFO, WARNING, ERROR.
max_rows int | None 20 Max DataFrame rows in log preview.
max_cols int | None 20 Max DataFrame columns in log preview.
mode str "text" "text" for human-readable, "json" for structured output.
file_mode str "w" File open mode. "w" = overwrite, "a" = append.

Methods

Method Description
debug(message, df=None) Detailed diagnostic info
info(message, df=None) Standard pipeline progress
warning(message, df=None) Data quality flags
error(message, df=None) Critical failures

Text Mode Output

2025-07-01 14:32:01 | INFO
Drop Nulls | (100, 5) → (87, 5) | 0.03s
Shape: (87, 5)
   col_a  col_b
0      1    3.4

JSON Mode Output

{"timestamp": "2025-07-01 14:32:01", "level": "INFO", "message": "Drop Nulls | (100, 5) → (87, 5) | 0.03s", "shape": [87, 5], "columns": ["col_a", "col_b"], "preview": {"col_a": [1], "col_b": [3.4]}}
# Text logger
logger = DFLogger(log_file="pipeline.log")

# JSON logger
json_logger = DFLogger(log_file="pipeline.json.log", mode="json")

# WARNING and above only
warning_logger = DFLogger(log_file="prod.log", min_level="WARNING")

@step Decorator

Module: dfflow.decorators.step_decorator Import: from dfflow import step

@step(name: str, cacheable: bool = False)

Attaches pipeline metadata to a function so it can be passed directly to pipeline.add_step().

from dfflow import step, FlowPipeline

@step("Remove Duplicates", cacheable=True)
def dedup(df):
    return df.drop_duplicates()

pipeline = FlowPipeline()
pipeline.add_step(dedup)

Built-in Cleaning Steps

Import: from dfflow import drop_nulls, lowercase_columns

drop_nulls

Removes all rows containing at least one null value. Decorated as @step("Drop Nulls", cacheable=True).

result = drop_nulls(df)

lowercase_columns

Converts all column names to lowercase. Decorated as @step("Lowercase Columns").

result = lowercase_columns(df)

DataProfiler

Module: dfflow.profiling.profiler Import: from dfflow import DataProfiler

DataProfiler.summary(df: pd.DataFrame) -> dict[str, object]
Return Key Type Description
shape tuple (rows, columns)
column_names list[str] Column names
null_counts dict[str, int] Null count per column
dtypes dict[str, str] Data type per column
memory_mb float Total memory in MB
duplicated_rows int Fully duplicate row count
from dfflow import DataProfiler

summary = DataProfiler.summary(df)
for key, value in summary.items():
    print(f"{key}: {value}")

DataFrame Diff Engine

Module: dfflow.diff.dataframe_diff

Six diff functions + one impact scorer. Called automatically with show_observability=True, but usable standalone.

Layer 1 — Structure

Function Returns
shape_diff(before, after) Row/column counts before/after, added/removed
column_diff(before, after) Columns added/removed

Layer 2 — Data Observability

Function Returns
null_diff(before, after) Null counts before/after, added/removed
memory_diff(before, after) Memory in MB before/after, delta
duplicate_diff(before, after) Duplicate counts before/after, added/removed
dtype_diff(before, after) Columns with changed dtypes

Layer 3 — Impact Scoring

impact_score(before_df, after_df) -> dict[str, object]
Condition Severity
Row change > 20% or null increase > 10% or memory change > 50% or dtype changed or duplicates added > 1000 HIGH
Row change > 5% or null increase > 2% or memory change > 20% or duplicates added > 100 MEDIUM
All other cases LOW
from dfflow.diff.dataframe_diff import impact_score

result = impact_score(before, after)
print(result["impact_level"])    # HIGH / MEDIUM / LOW
print(result["row_change_pct"])  # e.g. 35.0

Observability Formatter

Module: dfflow.diff.observability_formatter

Aggregates all diff functions into a single human-readable report. Called automatically when show_observability=True.

[OBSERVABILITY] Drop Nulls
Impact: HIGH

Rows: 1000 → 650 (-35.00%)
Nulls Added: 0
Nulls Removed: 350

Memory: 0.128MB → 0.083MB
Memory Change: -0.045 MB

Duplicates Added: 0
Duplicates Removed: 12

Dtype Changes: None

Pipeline Visualizer

Module: dfflow.viz.pipeline_viz

text_visualize(nodes: list[Step | Branch]) -> str

Generates a dependency-free ASCII diagram. Returns "Pipeline: (empty)" for empty pipelines.


Hashing Utility

Module: dfflow.utils.hashing

hash_df(df: pd.DataFrame) -> str

Generates a deterministic MD5 fingerprint of DataFrame contents. Used internally for step caching. Falls back to string casting for unhashable column types.


Exceptions

Module: dfflow.core.exceptions

PipelineStepError

Raised when a step raises an exception, returns a non-DataFrame, or a branch condition raises an exception.

from dfflow.core.exceptions import PipelineStepError

try:
    pipeline.run(df)
except PipelineStepError as e:
    print(e.step_name)  # Name of the failed step
    print(str(e))       # "Pipeline step failed: {step_name}"

Examples

Practical usage examples are available in the examples/ directory, including:

File Demonstrates
01_basic_pipeline.py FlowPipeline, @step, visualize()
02_logging.py DFLogger text and JSON modes
03_branching.py Branch, multiple conditions
04_observability.py cacheable=True, DataProfiler, show_observability
05_sales_report.py All features — demo sales pipeline

License

This project is licensed under the MIT License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dfflow-0.3.0.tar.gz (24.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dfflow-0.3.0-py3-none-any.whl (21.1 kB view details)

Uploaded Python 3

File details

Details for the file dfflow-0.3.0.tar.gz.

File metadata

  • Download URL: dfflow-0.3.0.tar.gz
  • Upload date:
  • Size: 24.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for dfflow-0.3.0.tar.gz
Algorithm Hash digest
SHA256 2797eb6ba7ac89c962886fbbc806bfc60a3b59b52d3f26021b16ddbb87324606
MD5 e87f6aac2ab24e1e0061d6a42729b4cd
BLAKE2b-256 3785d88c8cf92b017bd23535825b7665fd5c169dd114eca13d25a8e0eedc6e98

See more details on using hashes here.

File details

Details for the file dfflow-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: dfflow-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 21.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for dfflow-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ea96a0723195eff6da7d4aa26c33939f4479f2504ee14296ee8b8f8321159bdc
MD5 23df4a2fe37a8133b07e964cfaefd6ee
BLAKE2b-256 6bccb692218823bbe69928e469a1164be7de025ac9018478826064b06be712f2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page