Metadata-Version: 2.4
Name: safeframe
Version: 0.1.1
Summary: Catches silent pandas bugs (merge row-explosion, dtype coercion, NaN injection, index misalignment, chained assignment) at runtime, before they wreck your analysis.
Author: MohammadAmanDA
License: MIT
Project-URL: Homepage, https://github.com/MohammadAmanDA/tabular-code-critic
Keywords: pandas,data-quality,data-validation,llm,data-analysis
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: pandas>=1.3
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"

# 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'] = 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

```powershell
pip install safeframe
```

Or install straight from GitHub / from source — see
[Development](#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.

```powershell
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.

```python
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:

```python
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.

```powershell
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

```powershell
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.
