Catches silent pandas bugs (merge row-explosion, dtype coercion, NaN injection, index misalignment, chained assignment) at runtime, before they wreck your analysis.
Project description
safeframe
Pandas runs your code. It doesn't tell you when it silently wrecked your data.
safeframe watches pandas code while it runs and flags the bugs that don't
raise an exception or print a warning you'd notice — the ones that just quietly
give you the wrong answer:
- Merge/join row-explosion — duplicate keys silently multiply your rows.
- Silent NaN injection — unmatched join keys quietly fill columns with NaN.
- Silent dtype coercion — one stray value flips a numeric column to text;
.sum()returns garbage. - Index misalignment — arithmetic between misaligned indices silently introduces NaNs.
- Chained assignment no-ops —
df[mask]['col'] = xlooks like it worked. It didn't.
None of these crash. All of them are extremely common — whether the code was written by a human, or generated by an LLM you didn't fully review.
Install
pip install safeframe
Or install straight from GitHub / from source — see Development.
Zero-config mode (recommended)
Installing safeframe is enough on its own — no import, no code change. Every Python process started in that environment (your own terminal, a Claude Code / ChatGPT / OpenCode session, a Jupyter kernel — anything) silently watches pandas the moment pandas gets imported, for the life of that process, and prints findings straight to stderr as they happen. This is what makes it useful when an AI agent is the one writing and running the pandas code: whatever runs the code sees the warning in its own output, without needing to know safeframe exists.
pip install safeframe
python your_script.py # no changes needed - findings print automatically
It costs nothing for scripts that never touch pandas (the hook stays dormant
until pandas is actually imported). Disable it for a single process with
SAFEFRAME_DISABLE=1.
Manual mode
For scoped control (collecting a Report object to inspect programmatically,
or strict=True to raise instead of warn) use the watch() context manager
directly. Don't combine this with zero-config mode in the same process —
you'll get each finding printed twice, once per active guard.
import pandas as pd
import safeframe as sf
with sf.watch() as guard:
merged = orders.merge(customers, on="customer_id", how="left")
total = a + b
merged[merged["amount"] > 100]["status"] = "vip" # does nothing — see below
print(guard.report.render())
[!] [merge-explosion] merge(how='left') produced 5 rows from 4 (left) x 4 (right)
inputs - 1.2x more rows than expected for this join type. The right table
has 2 duplicate-key rows on ['customer_id'].
suggestion: Check for duplicate join keys, or pass validate='one_to_one' to merge().
[!] [nan-injection] merge(how='left'): 1/4 rows (25.0%) had no match on the
other side and were filled with NaN in ['name'].
suggestion: If this is unexpected, check for key mismatches before merging.
[x] [chained-assignment] chained assignment detected (df[mask]['col'] = value
style) - this sets a value on a temporary copy and does NOT modify the
original DataFrame.
suggestion: Use a single .loc assignment instead: df.loc[mask, "col"] = value
Run python examples/demo.py to see all five detectors fire on a single
realistic script.
By default findings are warnings (guard.report collects them, and each also
surfaces as a SafeFrameWarning). Pass strict=True to raise a
SilentBugError on the first error-severity finding instead:
with sf.watch(strict=True):
...
Also included: static lint mode
Before code even runs, safeframe lint checks a pandas snippet against a
real CSV's schema — flags columns referenced in code that don't actually
exist (a common LLM-hallucination failure mode), and flags slow
iterrows()/itertuples()/range(len(df)) loops with a vectorized rewrite
suggestion where one can be inferred.
safeframe lint data.csv script.py
Why this exists
Existing tools split into two camps: data-validation frameworks (Great
Expectations, Pandera) check whether your data matches a schema you wrote;
static linters (ruff, pandas-vet) check your source code without knowing
anything about the DataFrame it operates on. Neither catches an operation
that silently corrupts good data through a bad merge, a bad dtype coercion,
or a chained assignment that never lands. safeframe sits in that gap —
schema-aware, execution-aware, and framework-agnostic.
Development
python -m venv .venv
.\.venv\Scripts\activate
pip install -e ".[dev]"
pytest
Project status
MVP. Five detectors, a watch() runtime guard, zero-config auto-activation
on install (see above), and a lint static-check subcommand (the original
AST-based analyzer this project started as). See examples/demo.py for a
live walkthrough.
Note: zero-config mode only kicks in on a real (non-editable) install, since
editable installs don't process the .pth file it relies on — pip install
-e . for development gets you the manual watch() API only, which is what
the test suite uses.
Tested against pandas 2.1.4 and 3.0.3 (all 20 tests pass on both, and the
chained-assignment detector correctly handles both pandas <3.0's
SettingWithCopyWarning and pandas 3.0+'s ChainedAssignmentError). Verified
against a battery of hand-written safe/buggy fixtures with zero false
positives or negatives. Not yet validated against a large corpus of real,
unmodified public notebooks — treat findings on unusual code as worth a
second look until that validation happens.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file safeframe-0.1.1.tar.gz.
File metadata
- Download URL: safeframe-0.1.1.tar.gz
- Upload date:
- Size: 20.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b5a9eac4763373185a24965d80415a96ab3ce5d68d758c2ed1eb9304c4522bdc
|
|
| MD5 |
52395961668f76c7d4eec4463d30acc3
|
|
| BLAKE2b-256 |
372050b2e5638c010d614524a2e41f82f46d329030393072eeb101a620d3d904
|
File details
Details for the file safeframe-0.1.1-py3-none-any.whl.
File metadata
- Download URL: safeframe-0.1.1-py3-none-any.whl
- Upload date:
- Size: 20.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ba8868a00967b03120a634c82fb2eb866a60d343dbd3c1cd129b65529d6f7063
|
|
| MD5 |
924e2e187208d1336b7b7289ac7f5936
|
|
| BLAKE2b-256 |
1a0119364037b0f51858ef6a169ac1988aa332bb930313125fbe6f706176d128
|