FastFrame
FastFrame finds slow pandas/NumPy idioms, rewrites only safe patterns deterministically, and proves equivalence before reporting performance.
Problem
Row-wise apply, iterrows, repeated DataFrame concatenation, and repeated
np.append hide vectorizable work behind Python loops or repeated allocation.
FastFrame scans Python files, directories, and notebook code cells; applies
conservative LibCST rewrites; executes both versions in subprocesses; and
compares their results and stdout. Runtime has no LLM or network dependency.
Install
End users
After FastFrame is published to PyPI, a user with Python 3.12 can install it without cloning this repository:
python -m pip install fastframe
This installs the fastframe command. For example:
fastframe scan my_script.py
fastframe fix my_script.py -o my_script_fast.py
fastframe verify my_script.py my_script_fast.py
fastframe bench my_script.py my_script_fast.py --runs 5
FastFrame executes supplied scripts during fix verification, verify, and
bench; do not run it on untrusted scripts without a separate sandbox.
Maintainers: publish with GitHub Actions
Releases are published through PyPI Trusted Publishing, so no PyPI API token is stored in GitHub. Configure a pending publisher in PyPI with these values:
| PyPI field | Value |
|---|---|
| PyPI project name | fastframe-pandas |
| Owner | anurag-23werty |
| Repository name | FastFrame |
| Workflow name | .github/workflows/publish.yml |
| Environment name | pypi |
The pypi environment must also exist in the GitHub repository under
Settings → Environments. After the pending publisher is configured, bump
the version in pyproject.toml, update uv.lock, commit the release, and push
a matching tag:
uv lock
git add pyproject.toml uv.lock
git commit -m "release: v0.1.1"
git tag v0.1.1
git push origin main --tags
The workflow builds both the wheel and source distribution and publishes them
when the tag reaches GitHub. The tag version and pyproject.toml version must
match, and a version already uploaded to PyPI cannot be reused.
uv
For contributors working from the source repository:
uv sync --locked --extra test
Real output from this repository:
Resolved 31 packages in 29ms
Checked 30 packages in 13ms
Docker
docker build -t fastframe:buildathon .
The image runs as non-root user fastframe and has fastframe as its
entrypoint. Run a scan with:
docker run --rm --network none fastframe:buildathon scan examples/sales_analysis_slow.py
Run the complete offline test command by overriding the CLI entrypoint:
docker run --rm --network none --entrypoint make fastframe:buildathon test
Terminal demo
$ uv run fastframe scan examples/sales_analysis_slow.py
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ Location ┃ Pattern ┃ Explanation ┃ Suggested action ┃
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│ examples/sales_anal… │ AP001 │ row-wise │ vectorize with │
│ │ │ DataFrame.apply │ fastframe fix │
│ │ │ arithmetic │ │
│ examples/sales_anal… │ AP002 │ Series.apply │ vectorize with │
│ │ │ arithmetic lambda │ fastframe fix │
│ examples/sales_anal… │ AP003 │ iterrows arithmetic │ vectorize with │
│ │ │ append-to-list │ fastframe fix │
│ examples/sales_anal… │ AP004 │ growing DataFrame │ vectorize with │
│ │ │ with concat/append │ fastframe fix │
│ │ │ in a loop │ │
│ examples/sales_anal… │ AP005 │ growing NumPy array │ vectorize with │
│ │ │ with np.append in a │ fastframe fix │
│ │ │ loop │ │
│ examples/sales_anal… │ AP006 │ downcastable │ manual review │
│ │ │ object/int64/float64 │ │
│ │ │ column (manual │ │
│ │ │ review) │ │
└──────────────────────┴─────────┴──────────────────────┴──────────────────────┘
EXIT_CODE=1
$ uv run fastframe fix examples/sales_analysis_slow.py -o build/sales_fast.py
Verification passed.
--- examples/sales_analysis_slow.py
+++ build/sales_fast.py
@@
-row_totals = work.apply(lambda row: row["units"] * row["price"], axis=1)
+row_totals = work["units"] * work["price"]
CLI
Scan
Scan a file, directory recursively, or notebook code cells:
uv run fastframe scan examples/sales_analysis_slow.py
uv run fastframe scan src --format json
scan --format json emits machine-readable findings and exits 1 when any
finding exists, otherwise 0.
Inline suppressions are supported:
out = df["a"].apply(lambda x: x * 2) # fastframe: ignore
out = df["b"].apply(lambda x: x * 2) # fastframe: ignore[AP002]
Project configuration lives in pyproject.toml:
[tool.fastframe]
select = ["AP001", "AP002"]
ignore = ["AP006"]
Fix
Fix verifies by default and refuses to write if verification fails:
uv run fastframe fix examples/sales_analysis_slow.py -o build/sales_fast.py
uv run fastframe fix examples/sales_analysis_slow.py --diff
uv run fastframe fix examples/sales_analysis_slow.py --in-place
uv run fastframe fix examples/sales_analysis_slow.py -o build/sales_fast.py --no-verify
--in-place keeps sales_analysis_slow.py.bak. --no-verify prints a warning.
Notebook fixes write rewritten code cells to a copy; Markdown cells are kept.
Verify
uv run fastframe verify examples/sales_analysis_slow.py build/sales_fast.py
uv run fastframe verify examples/sales_analysis_slow.py build/sales_fast.py --json
Real readable output:
PASS verification
┏━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━┓
┃ Variable ┃ Status ┃ Details ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━┩
│ amounts │ PASS │ equivalent │
│ combined │ PASS │ equivalent │
│ row_totals │ PASS │ equivalent │
│ scaled_units │ PASS │ equivalent │
│ values │ PASS │ equivalent │
└──────────────┴────────┴────────────┘
stdout: PASS
Verification reports original variables missing from the rewritten script as
failures, compares captured stdout, uses the original script directory for
relative paths and __file__, and has a 60-second subprocess timeout.
Bench
uv run fastframe bench examples/sales_analysis_slow.py build/sales_fast.py --runs 1
uv run fastframe bench examples/sales_analysis_slow.py build/sales_fast.py --runs 1 --json
Real output from one run on the 200k-row example:
┏━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓
┃ Script ┃ Median ┃ Minimum ┃ Peak memory ┃
┡━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩
│ Original │ 1.1700s │ 1.1700s │ 22,755,632 bytes │
│ Rewritten │ 0.0633s │ 0.0633s │ 22,753,741 bytes │
└───────────┴─────────┴─────────┴──────────────────┘
Speedup: 18.50x
Peak memory saved: +1,891 bytes (+0.01%)
Timing and memory are measured in separate child runs. Imports happen before timing, and each measurement path gets one warm-up run.
Report, rules, and explain
uv run fastframe report examples/sales_analysis_slow.py --html docs/index.html --runs 1
uv run fastframe rules
uv run fastframe explain AP003
report writes a self-contained HTML report. rules lists every built-in
rule. explain AP003 prints its explanation and representative before/after.
Supported rules
AP001 — row-wise DataFrame arithmetic
# Before
out = df.apply(lambda row: row["a"] + row["b"], axis=1)
# After
out = df["a"] + df["b"]
AP002 — arithmetic Series lambda
# Before
out = df["a"].apply(lambda x: x * 2)
# After
out = df["a"] * 2
AP003 — iterrows() arithmetic append
# Before
for _, row in df.iterrows():
out.append(row["a"] + row["b"])
# After
out.extend((df["a"] + df["b"]).tolist())
AP004 — DataFrame concat growth
# Before
for chunk in chunks:
df = pd.concat([df, chunk], ignore_index=True)
# After
_fastframe_df_parts = [df]
for chunk in chunks:
_fastframe_df_parts.append(chunk)
if len(_fastframe_df_parts) > 1:
df = pd.concat(_fastframe_df_parts, ignore_index=True)
AP005 — np.append growth
# Before
for value in values:
arr = np.append(arr, value)
# After
_fastframe_arr_parts = [arr]
for value in values:
_fastframe_arr_parts.append(np.atleast_1d(value))
if len(_fastframe_arr_parts) > 1:
arr = np.concatenate(_fastframe_arr_parts)
AP006 — downcast advisory
# Finding; not automatically rewritten
df["count"] = df["count"].astype("int64")
# Manual review suggestion
df["count"] = pd.to_numeric(df["count"], downcast="integer")
Results: 200k-row example
examples/sales_analysis_slow.py creates 200,000 rows with seeded RNG
np.random.default_rng(2026). The report keeps the combined script as the
whole-workload measurement, while each AP001–AP005 row uses its own focused
100k-row script from examples/patterns/. The following values all came from
one --runs 1 report generation:
| Benchmark | Workload | Speedup | Verification |
|---|---|---|---|
| AP001 row-wise apply | examples/patterns/ap001.py |
147.32× | PASS |
| AP002 Series apply | examples/patterns/ap002.py |
12.01× | PASS |
| AP003 iterrows append | examples/patterns/ap003.py |
436.25× | PASS |
| AP004 concat growth | examples/patterns/ap004.py |
6.23× | PASS |
AP005 np.append growth |
examples/patterns/ap005.py |
5.88× | PASS |
| AP006 downcast advisory | no rewrite | advisory only | manual review |
| Whole sales workload | examples/sales_analysis_slow.py (200k rows) |
18.50× | PASS |
Benchmarks include subprocess execution and are machine/load dependent. The focused scripts are sized to make the slow form measurable; AP004 uses 2,000 prebuilt 500-row chunks and AP005 uses 20,000 appends. The HTML report contains the complete per-pattern timing, memory, code, diff, and badge details.
Safety guarantees
- Only a strict arithmetic whitelist is rewritten: string-literal parameter
subscripts, permitted bare parameters, numeric constants, unary minus, and
+ - * / // ** %. - Calls involving
groupby,rolling,resample,expanding, orewmare never automatically rewritten. pd.concatrewrites require exactly[target, piece].np.appendrewrites require the target as the first argument and rejectaxis.iterrowsrewrites require_as the index variable; index-dependent expressions are never rewritten.- AP006 is always advisory because safe downcasting depends on values, nullability, precision, and downstream consumers.
- Rewrites preserve source formatting with LibCST and
fixverifies before writing by default.
Limitations & security
verifyandbenchexecute both supplied scripts withrunpyin child processes. They are not a security sandbox.- Do not run untrusted scripts with FastFrame. Use a container or another sandbox with restricted filesystem, network, CPU, and memory access.
- Verification compares matching top-level DataFrame, Series, ndarray, and list variables plus exact captured stdout. External side effects, databases, files, and network behavior are not equivalence guarantees.
- Benchmarks measure complete script execution after imports, not an isolated function microbenchmark.
- Unknown source shapes are reported only when recognized; no speculative rewrite is attempted.
- Rewrites are syntax-based rather than type-aware: a method that looks like
pandas
applyon a non-pandas object can be rewritten. The defaultfixverification step executes both versions and catches mismatched behavior before writing the output.
Tests and quality
make test
uv run --no-sync ruff check .
uv run --no-sync mypy
Real local result:
90 passed
Required test coverage of 90% reached. Total coverage: 94.39%
GitHub Actions runs Ruff, mypy, coverage tests, Docker build, and the offline Docker test on pushes and pull requests.
Live demo
Metadata
Release files for fastframe-pandas 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| fastframe_pandas-0.1.1.tar.gz | 38.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fastframe_pandas-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 64.3 kB
Release files / fastframe_pandas-0.1.1.tar.gz
| Download URL | fastframe_pandas-0.1.1.tar.gz |
|---|---|
| Size | 38.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bca9fc27f4d51f5246762a45081a37d83783df452de223e7dbf851c6d3b5f170
|
|
BLAKE2b-256 checksum How to use checksums |
17b1e082040e6005975496493f9346a7c2a4d109ebe4a388ea4e7ca8af68c940
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logRelease files / fastframe_pandas-0.1.1-py3-none-any.whl
| Download URL | fastframe_pandas-0.1.1-py3-none-any.whl |
|---|---|
| Size | 26.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bf53c40fe20aca67c0dcb9425749129b50cb6da248bdc225edc16159466f1985
|
|
BLAKE2b-256 checksum How to use checksums |
6cdd9d616aae2cb742ce23a6b9f030abaccc5fef7eb7299c380cc04f6583d2b5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logSecurity for fastframe-pandas 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for fastframe_pandas-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.