Skip to main content

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-opsdf[mask]['col'] = x looks 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

(Not yet published to PyPI — install straight from GitHub in the meantime: pip install git+https://github.com/MohammadAmanDA/tabular-code-critic.git, or see Development to run from source.)

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 git+https://github.com/MohammadAmanDA/tabular-code-critic.git
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


Download files

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

Source Distribution

safeframe-0.1.0.tar.gz (20.6 kB view details)

Uploaded Source

Built Distribution

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

safeframe-0.1.0-py3-none-any.whl (19.9 kB view details)

Uploaded Python 3

File details

Details for the file safeframe-0.1.0.tar.gz.

File metadata

  • Download URL: safeframe-0.1.0.tar.gz
  • Upload date:
  • Size: 20.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.5

File hashes

Hashes for safeframe-0.1.0.tar.gz
Algorithm Hash digest
SHA256 5e8c85f42ee56ac8f2d6bf747c67e1accaca0a063a70a57997aa32118e8f6832
MD5 b1ef1479d7e9b06af67e0655ce36315e
BLAKE2b-256 46daa63e2d8b0cdc7a56cc4c678463114f2279827cc87590d6ddd9e493d2194e

See more details on using hashes here.

File details

Details for the file safeframe-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: safeframe-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 19.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.5

File hashes

Hashes for safeframe-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ec58340761d0f4a92c94d0976b5374b6a958d7a4968a9854635b3b32f7cc7932
MD5 5d5f6b6897ba5544f5f5b40a7a13cdcd
BLAKE2b-256 f367f08477cfcc6d58812156a0ebdeb8f41fb7707d1b049b38304491f3eb162c

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