pandas 3.0 removed the APIs deprecated through 2.x and made two behaviour changes permanent. Copy-on-write is always on, so chained assignment such as df["v"][0] = 1, or df["v"].fillna(0, inplace=True), changes a temporary copy and leaves the frame untouched, with only a warning. Strings are now the str dtype, so dtype == object checks quietly stop matching. Agents learnt pandas from a decade of answers that rely on all of this.
This loadout pairs a short Cursor rule and AGENTS.md (a never/always table) with a skill that checks the work. A standard-library Python scanner reports removed APIs (applymap, append, fillna(method=), delim_whitespace, date_parser, old offset aliases such as H and M, errors="ignore") and chained writes as errors, and object-dtype checks and agg(np.sum) as warnings. Then the tests run with ChainedAssignmentError raised as an error, so every lost write fails. The verifier runs the scanner against a pandas 3 script and a pandas 2 sample, and runs both with the pinned pandas 3.1.0.
What's in it
- Never/always table for pandas 3 in a .py-scoped Cursor rule
- Scanner flags removed APIs and chained writes as errors, read-only
- Catches writes copy-on-write throws away, like df["v"][0] = 1
- Tests run with ChainedAssignmentError as an error, so lost writes fail
- Warns on object-dtype string checks now that strings are dtype str
You'll need
- Tools:
python3,uv
Verification
| Check | Result | What we found |
|---|---|---|
| Listinglisting.yaml matches the schema and declares every file in the folder. | Pass | Schema valid; 8 files declared, none missing or extra
|
| Files parseJSON parses; SKILL.md and .mdc front matter is valid YAML with the fields each agent needs; code fences are closed. | Pass | All 8 files parse; front matter valid |
| AdaptersCLAUDE.md imports @AGENTS.md, GEMINI.md imports @./AGENTS.md, and both match the generator. | Pass | CLAUDE.md and GEMINI.md import AGENTS.md and are up to date
|
| Install pathsEvery file lands where each agent documents it, with no collisions, and the laid-out project is re-discovered correctly. | Pass | Correct paths for all 6 agents; layouts re-discovered
|
| MCP configmcp.json is structurally valid, converts to each agent's format, declares no literal secrets, and referenced packages exist on npm/PyPI. | Not applicable | No MCP servers in this loadout |
| Safety scanThe intake scanner finds no credentials, risky shell, hidden Unicode, instructions hidden in HTML comments or after long blank padding, or prompt-injection and covert wording (such as telling the agent to keep something from you), and the listing's network flag is honest. | Pass | Clean: no credentials, risky shell, hidden text or covert instructions
|
| ReferencesLinks and file paths between the loadout's files resolve, including for each agent's installed layout. | Pass | 3 references between files resolve for every agent |
| Size limitsFile count and sizes are within the submission limits; SKILL.md bodies stay under 500 lines. | Pass | 8 files, 18.5 KB total; largest skills/pandas-3-guard/scripts/pandas3_guard.py 8.0 KB
|
| LicenceA recognised SPDX licence, with the licence text available. | Pass | MIT, by SetupBay; text in the repository LICENSE |
| Starter buildA minimal fixture project with the loadout applied installs and builds or type-checks, and the loadout's own code examples compile. | Pass | pandas3_guard.py passes a pandas 3 cleaning script, catches the deliberate chained write in its test, and flags every pandas 2 line in the skill's sample: 10 removed calls and lost writes as errors, 2 meaning changes as warnings. The pinned pandas 3.1.0 runs the script and its tests with ChainedAssi
|
What this means: a script checked the files themselves: they parse, they install where each agent looks, any MCP config is sound, the safety scan is clean, references between files resolve, and, where a free and repeatable fixture exists, a small starter project builds with the loadout applied. No AI agent was run. Per-agent results from agent runs are separate and stay untested until an agent actually runs.
Install
Choose your agent. Run the commands from your project root, or download a zip already laid out in the right folders.
Claude Code reads CLAUDE.md (imports AGENTS.md with @AGENTS.md) and skills from .claude/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/AGENTS.md curl -fsSL --create-dirs -o CLAUDE.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/CLAUDE.md curl -fsSL --create-dirs -o .claude/skills/pandas-3-guard/SKILL.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/SKILL.md curl -fsSL --create-dirs -o .claude/skills/pandas-3-guard/scripts/pandas3_guard.py https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/scripts/pandas3_guard.py curl -fsSL --create-dirs -o .claude/skills/pandas-3-guard/references/rename-table.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/references/rename-table.md curl -fsSL --create-dirs -o .claude/skills/pandas-3-guard/references/pandas2-sample.py.txt https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/references/pandas2-sample.py.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
CLAUDE.md | CLAUDE.md |
skills/pandas-3-guard/SKILL.md | .claude/skills/pandas-3-guard/SKILL.md |
skills/pandas-3-guard/scripts/pandas3_guard.py | .claude/skills/pandas-3-guard/scripts/pandas3_guard.py |
skills/pandas-3-guard/references/rename-table.md | .claude/skills/pandas-3-guard/references/rename-table.md |
skills/pandas-3-guard/references/pandas2-sample.py.txt | .claude/skills/pandas-3-guard/references/pandas2-sample.py.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Claude Code reads.
Codex CLI reads AGENTS.md and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/SKILL.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/SKILL.md curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/scripts/pandas3_guard.py https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/scripts/pandas3_guard.py curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/references/rename-table.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/references/rename-table.md curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/references/pandas2-sample.py.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/pandas-3-guard/SKILL.md | .agents/skills/pandas-3-guard/SKILL.md |
skills/pandas-3-guard/scripts/pandas3_guard.py | .agents/skills/pandas-3-guard/scripts/pandas3_guard.py |
skills/pandas-3-guard/references/rename-table.md | .agents/skills/pandas-3-guard/references/rename-table.md |
skills/pandas-3-guard/references/pandas2-sample.py.txt | .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Codex CLI reads.
Cursor reads AGENTS.md + .cursor/rules/*.mdc and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .cursor/rules/pandas-3-only.mdc https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/.cursor/rules/pandas-3-only.mdc curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/SKILL.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/SKILL.md curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/scripts/pandas3_guard.py https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/scripts/pandas3_guard.py curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/references/rename-table.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/references/rename-table.md curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/references/pandas2-sample.py.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
.cursor/rules/pandas-3-only.mdc | .cursor/rules/pandas-3-only.mdc |
skills/pandas-3-guard/SKILL.md | .agents/skills/pandas-3-guard/SKILL.md |
skills/pandas-3-guard/scripts/pandas3_guard.py | .agents/skills/pandas-3-guard/scripts/pandas3_guard.py |
skills/pandas-3-guard/references/rename-table.md | .agents/skills/pandas-3-guard/references/rename-table.md |
skills/pandas-3-guard/references/pandas2-sample.py.txt | .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Cursor reads.
Gemini CLI reads GEMINI.md (imports AGENTS.md with @./AGENTS.md) and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/AGENTS.md curl -fsSL --create-dirs -o GEMINI.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/GEMINI.md curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/SKILL.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/SKILL.md curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/scripts/pandas3_guard.py https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/scripts/pandas3_guard.py curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/references/rename-table.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/references/rename-table.md curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/references/pandas2-sample.py.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
GEMINI.md | GEMINI.md |
skills/pandas-3-guard/SKILL.md | .agents/skills/pandas-3-guard/SKILL.md |
skills/pandas-3-guard/scripts/pandas3_guard.py | .agents/skills/pandas-3-guard/scripts/pandas3_guard.py |
skills/pandas-3-guard/references/rename-table.md | .agents/skills/pandas-3-guard/references/rename-table.md |
skills/pandas-3-guard/references/pandas2-sample.py.txt | .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Gemini CLI reads.
Antigravity CLI reads AGENTS.md (read natively; GEMINI.md is also read) and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/SKILL.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/SKILL.md curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/scripts/pandas3_guard.py https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/scripts/pandas3_guard.py curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/references/rename-table.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/references/rename-table.md curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/references/pandas2-sample.py.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/pandas-3-guard/SKILL.md | .agents/skills/pandas-3-guard/SKILL.md |
skills/pandas-3-guard/scripts/pandas3_guard.py | .agents/skills/pandas-3-guard/scripts/pandas3_guard.py |
skills/pandas-3-guard/references/rename-table.md | .agents/skills/pandas-3-guard/references/rename-table.md |
skills/pandas-3-guard/references/pandas2-sample.py.txt | .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Antigravity CLI reads.
GitHub Copilot reads AGENTS.md and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/SKILL.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/SKILL.md curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/scripts/pandas3_guard.py https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/scripts/pandas3_guard.py curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/references/rename-table.md https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/references/rename-table.md curl -fsSL --create-dirs -o .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt https://setupbay.com/raw/pandas-3-upgrade-guard/1.0.0/skills/pandas-3-guard/references/pandas2-sample.py.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/pandas-3-guard/SKILL.md | .agents/skills/pandas-3-guard/SKILL.md |
skills/pandas-3-guard/scripts/pandas3_guard.py | .agents/skills/pandas-3-guard/scripts/pandas3_guard.py |
skills/pandas-3-guard/references/rename-table.md | .agents/skills/pandas-3-guard/references/rename-table.md |
skills/pandas-3-guard/references/pandas2-sample.py.txt | .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what GitHub Copilot reads.
Or as a Claude Code plugin
Adds the skill to Claude Code for all your projects, without copying files. Plugins can't carry project rules, so add AGENTS.md and CLAUDE.md with the commands above. About the plugin marketplace.
/plugin marketplace add https://setupbay.com/marketplace.json /plugin install pandas-3-upgrade-guard@setupbay
Files
Rules AGENTS.md pandas 3 version facts, rules and the done checklist. 32 lines
# AGENTS.md: pandas 3 project (pandas 1 and 2 guard)
This project uses **pandas 3** (`pandas>=3.1,<4`). Most pandas code you have seen predates it. pandas 3 removed a
long list of deprecated APIs and changed two things that make old code wrong without an error:
**copy-on-write is always on**, and **strings are the `str` dtype, not `object`**.
## Version facts
- Check the pin first: `uv pip show pandas` or the `pandas` line in `pyproject.toml`/`uv.lock`.
- Copy-on-write: every indexing result behaves as a copy. Chained assignment (`df["a"][0] = 1`) and
`df["a"].fillna(0, inplace=True)` change a temporary copy and leave `df` alone. pandas only warns.
- `pd.Series(["a"]).dtype` is `str`. `dtype == object` checks are now false for string columns, and a
string column refuses non-string values.
- Offset aliases: `h`, `min`, `s`, `ms`, `us`, `ns`; month, quarter and year ends are `ME`, `QE`, `YE`.
- If you are unsure an API exists in pandas 3, check the API reference (https://pandas.pydata.org/docs/reference/)
or the 3.0 release notes (https://pandas.pydata.org/docs/whatsnew/v3.0.0.html). Don't copy pandas 1 answers.
## Rules
- Write pandas 3 only. The `pandas-3-guard` skill has the full table; the common traps are:
`DataFrame.map` (not `applymap`), `pd.concat` (not `append`), `.ffill()` (not `fillna(method=)`),
`sep=r"\s+"` (not `delim_whitespace`), `date_format=` (not `date_parser`), `freq="h"`/`"ME"` (not `"H"`/`"M"`),
`errors="coerce"` (not `"ignore"`), `pd.read_json(io.StringIO(text))` (not literal JSON).
- Assign with one `.loc` call: `df.loc[mask, "col"] = value`. Never chain `[...][...] =`.
- Reassign instead of `inplace=True` on a column: `df["col"] = df["col"].fillna(0)`.
- Test for strings with `pd.api.types.is_string_dtype(s)`, not `dtype == object`.
- Don't upgrade or downgrade pandas without asking the human.
## Before you say "done"
1. Run the `pandas-3-guard` scanner on the files you touched; it must report `0 error(s)`. Read every `warn`.
2. Run the tests with warnings as errors for pandas' own warnings:
`uv run pytest -W error::pandas.errors.ChainedAssignmentError`.
3. Say which silent changes (copy-on-write writes, string dtype checks) you looked for and where. Adapter CLAUDE.md Imports AGENTS.md for Claude Code. 11 lines
# CLAUDE.md
@AGENTS.md
## Claude Code notes
- Skills from this loadout live in `.claude/skills/`: `pandas-3-guard`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.
<!-- From SetupBay "pandas 3 guard for copy-on-write and removed APIs" v1.0.0 — https://setupbay.com/loadouts/pandas-3-upgrade-guard/ --> Adapter GEMINI.md Imports AGENTS.md for Gemini CLI. 11 lines
# GEMINI.md
@./AGENTS.md
## Gemini CLI notes
- Skills from this loadout live in `.agents/skills/`: `pandas-3-guard`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.
<!-- From SetupBay "pandas 3 guard for copy-on-write and removed APIs" v1.0.0 — https://setupbay.com/loadouts/pandas-3-upgrade-guard/ --> Cursor rule .cursor/rules/pandas-3-only.mdc Never/always table for Python files. 26 lines
---
description: pandas 3 only; copy-on-write and the str dtype, no APIs removed in 3.0
globs: "**/*.py,**/*.ipynb"
alwaysApply: false
---
This project pins pandas 3. Copy-on-write is always on and strings are dtype `str`.
| Never (pandas 1 and 2) | Always (pandas 3) |
|---|---|
| `df["a"][0] = 1` | `df.loc[0, "a"] = 1` |
| `df["a"].fillna(0, inplace=True)` | `df["a"] = df["a"].fillna(0)` |
| `df.applymap(f)` | `df.map(f)` |
| `df.append(other)` | `pd.concat([df, other], ignore_index=True)` |
| `fillna(method="ffill")` | `.ffill()` |
| `read_csv(delim_whitespace=True)` | `read_csv(sep=r"\s+")` |
| `read_csv(date_parser=...)` | `read_csv(date_format="%Y-%m-%d")` |
| `freq="H"`, `"T"`, `"M"`, `"Y"` | `freq="h"`, `"min"`, `"ME"`, `"YE"` |
| `to_numeric(s, errors="ignore")` | `to_numeric(s, errors="coerce")` |
| `pd.read_json('[{...}]')` | `pd.read_json(io.StringIO(text))` |
| `s.dtype == object` for strings | `pd.api.types.is_string_dtype(s)` |
| `agg(np.sum)` | `agg("sum")` |
- No defensive `.copy()` to silence SettingWithCopyWarning; that warning no longer exists.
- After editing, run the `pandas-3-guard` skill's scanner and the tests. Skill skills/pandas-3-guard/SKILL.md Scan for removed pandas APIs and lost writes, then run the tests with ChainedAssignmentError as an error. 44 lines
---
name: pandas-3-guard
description: Check Python for pandas APIs removed in 3.0, chained assignment that copy-on-write ignores, and object-dtype string checks, before saying a pandas change is done. Use after writing or editing pandas code, when pandas raises an unexpected keyword or attribute error, warns ChainedAssignmentError, or when porting older pandas snippets.
---
# pandas 3 guard
pandas 3.0 (21 January 2026; 3.1 on 22 October 2026) removed what 2.x deprecated and turned on two behaviour
changes for good. Copy-on-write means a chained write such as `df["v"][0] = 1` changes a copy and leaves `df`
alone, with only a warning. The `str` dtype means string columns are no longer `object`. Removed names fail loudly.
Those two changes don't fail at all.
## 1. Confirm the version
- `uv run python -c "import pandas; print(pandas.__version__)"` must print `3.x`. If it prints `2.x`, stop and ask
whether the project is moving to pandas 3. The pandas docs suggest getting clean on 2.3 first.
## 2. Scan
```bash
python3 .agents/skills/pandas-3-guard/scripts/pandas3_guard.py src tests notebooks
```
(Use the path the skill was installed to: `.claude/skills/...` for Claude Code, `.agents/skills/...` for the others.)
- Every `error` line is either an API pandas 3 removed or a write that copy-on-write throws away. Fix it with the
replacement the line gives; `references/rename-table.md` has the table.
- The chained-assignment rules only look at names like `df`, `*_df`, `frame*` and `data`, so a frame called
something else can slip through. The test run below catches those.
- `warn` lines still run but may mean something different now: `dtype == object` checks, `agg(np.sum)`,
groupby on categoricals. Fix them, or add `# guard: ok` to a line where the new behaviour is what you want.
That silences warnings, never errors.
## 3. Let pandas check it
```bash
uv run pytest -q -W error::pandas.errors.ChainedAssignmentError
```
- `ChainedAssignmentError` is a warning by default. Turning it into an error makes every lost write fail a test.
- `TypeError: ... got an unexpected keyword argument` and `AttributeError: 'DataFrame' object has no attribute`
mean a removed API. The release notes name each replacement.
- `TypeError: Invalid value ... for dtype 'str'` means code put a non-string into a string column. Convert on purpose
(`astype(object)`) only if mixed values are really wanted.
## 4. Report
Say how many removed calls and chained writes you replaced, which dtype checks you changed, and that the scanner and
the tests (with ChainedAssignmentError as an error) were clean. Skill file skills/pandas-3-guard/scripts/pandas3_guard.py Read-only scanner for removed pandas APIs, chained writes and dtype checks. 164 lines
#!/usr/bin/env python3
"""Find pandas 1.x/2.x code that pandas 3 removed, and code that copy-on-write or the str dtype quietly changed.
Usage: python3 pandas3_guard.py [PATH ...] (default: the current folder)
python3 pandas3_guard.py --list (print every rule)
A "guard: ok" comment on a line silences its warnings (never its errors).
Prints one line per finding: path:line: level [rule] what is wrong -> what to use
Exit 0 when nothing at "error" level is found, 1 when something is, 2 on bad usage.
Standard library only. Read-only: it never edits a file and makes no network calls.
"""
import fnmatch
import os
import re
import sys
# (rule id, level, regex, message, file patterns). Patterns run against each line with comments removed.
RULES = [
('applymap', 'error', '\\.applymap\\(',
'DataFrame.applymap() was removed -> DataFrame.map()', []),
('append', 'error', '\\b(df\\w*|frame\\w*|data\\w*|result\\w*|out\\w*)\\.append\\(',
'DataFrame.append() was removed (2.0) -> pd.concat([a, b], ignore_index=True)', []),
('iteritems', 'error', '\\.iteritems\\(',
'iteritems() was removed (2.0) -> items()', []),
('fillna-method', 'error', '\\.fillna\\([^)]*\\bmethod\\s*=',
"fillna(method='ffill') was removed -> .ffill() / .bfill()", []),
('replace-method', 'error', '\\.replace\\([^)]*\\b(method|limit)\\s*=',
'replace(method=..., limit=...) was removed -> .ffill() / .bfill() after replacing with NA', []),
('interpolate-pad', 'error', '\\.interpolate\\([^)]*[\\"\'](pad|ffill|bfill|backfill)[\\"\']',
"interpolate('ffill') was removed -> .ffill() / .bfill()", []),
('freq-alias', 'error', '\\b(freq|rule)\\s*=\\s*[\\"\'](\\d*)(H|BH|CBH|T|L|U|N|M|BM|SM|CBM|Q|BQ|Y|BY|A|BA|AS|BAS)(-[A-Z]{3})?[\\"\']',
"old offset alias -> 'h', 'min', 'ms', 'us', 'ns', 'ME', 'QE', 'YE' (month/quarter/year END aliases)", []),
('resample-alias', 'error', '\\.(resample|asfreq)\\(\\s*[\\"\'](\\d*)(H|T|M|Q|Y|A)[\\"\']',
"old offset alias in resample/asfreq -> 'h', 'min', 'ME', 'QE', 'YE'", []),
('delim-whitespace', 'error', '\\bdelim_whitespace\\s*=',
'read_csv(delim_whitespace=True) was removed -> sep=r"\\s+"', []),
('date-parser', 'error', '\\b(date_parser|infer_datetime_format|keep_date_col)\\s*=',
'read_csv date_parser / infer_datetime_format / keep_date_col were removed -> date_format=... or pd.to_datetime after reading', []),
('swapaxes', 'error', '\\.swapaxes\\(',
'swapaxes() was removed -> .T or .transpose()', []),
('series-view', 'error', '\\]\\.view\\(|\\bseries\\w*\\.view\\(|\\bs\\.view\\(',
'Series.view() was removed -> .astype(...)', []),
('groupby-axis', 'error', '\\.(groupby|rolling|resample)\\([^)]*\\baxis\\s*=',
'the axis= argument was removed from groupby/rolling/resample -> transpose first', []),
('first-last', 'error', '\\.(first|last)\\(\\s*[\\"\']\\d',
"DataFrame.first('3D') / last('3D') were removed -> df.loc[df.index < df.index[0] + pd.Timedelta('3D')]", []),
('errors-ignore', 'error', '\\bto_(numeric|datetime|timedelta)\\([^)]*errors\\s*=\\s*[\\"\']ignore[\\"\']',
"errors='ignore' was removed -> errors='coerce' and handle NaN, or try/except", []),
('series-bool', 'error', '\\.bool\\(\\s*\\)',
'Series.bool() / DataFrame.bool() were removed -> bool(s.item())', []),
('use-inf-as-na', 'error', 'use_inf_as_na',
'mode.use_inf_as_na was removed -> df.replace([np.inf, -np.inf], np.nan)', []),
('copy-on-write-option', 'warn', 'mode\\.copy_on_write|copy_on_write\\s*=',
'copy-on-write is always on in pandas 3; the option does nothing and will be removed', []),
('read-literal', 'error', 'pd\\.read_(json|html|xml|excel)\\(\\s*(f?[\\"\'][\\[{<]|response\\.text|r\\.text)',
'read_json/read_html/read_xml no longer take literal text -> wrap it: pd.read_json(io.StringIO(text))', []),
('chained-assignment', 'error', '\\b(df\\w*|\\w+_df|frame\\w*|data)\\[[^\\]]+\\]\\[[^\\]]+\\]\\s*=[^=]',
"chained assignment never writes back under copy-on-write -> df.loc[row, 'col'] = value", []),
('inplace-on-column', 'error', '\\b(df\\w*|\\w+_df|frame\\w*|data)\\[[^\\]]+\\]\\.\\w+\\([^)]*inplace\\s*=\\s*True',
"df['col'].method(inplace=True) changes a copy, not df -> df['col'] = df['col'].method()", []),
('object-dtype-check', 'warn', 'dtype\\s*==\\s*(object|[\\"\']object[\\"\']|np\\.object_)|select_dtypes\\([^)]*[\\"\']object[\\"\']',
"strings are now dtype 'str', not object -> pd.api.types.is_string_dtype(s) or select_dtypes(include=['str'])", []),
('setting-with-copy', 'warn', 'SettingWithCopyWarning|\\.copy\\(\\)\\s*#.*SettingWithCopy',
'SettingWithCopyWarning is gone in pandas 3; the defensive .copy() is not needed for it', []),
('observed-default', 'warn', '\\.groupby\\((?![^)]*observed)[^)]*\\)(?=.*cat)',
'groupby on categoricals now defaults to observed=True; pass observed=False to keep empty categories', []),
('future-stack', 'warn', '\\.stack\\([^)]*future_stack',
'future_stack is the default in pandas 3; drop the argument', []),
('np-reduction-in-agg', 'warn', '\\.(agg|apply|transform)\\(\\s*(np\\.(sum|mean|max|min|std)|sum|max|min)\\s*[,)]',
"agg(np.sum) is no longer swapped for pandas' own sum -> pass the string 'sum'", []),
]
FILES = ['*.py', '*.ipynb']
COMMENT = '#'
SKIP_DIRS = {".git", ".venv", "venv", "node_modules", "__pycache__", ".agents", ".claude", ".cursor", "dist", "build", "target", ".godot"}
COMPILED = [(rid, lvl, re.compile(rx), msg, globs) for rid, lvl, rx, msg, globs in RULES]
def strip_comment(line):
"""Drop a trailing comment, leaving string literals alone."""
q = None
i = 0
while i < len(line):
c = line[i]
if q:
if c == "\\":
i += 2
continue
if c == q:
q = None
elif c in "\"'`":
q = c
elif line.startswith(COMMENT, i):
return line[:i]
i += 1
return line
def wanted(name):
return any(fnmatch.fnmatch(name, g) for g in FILES)
def walk(paths):
for p in paths:
if os.path.isfile(p):
yield p
continue
for root, dirs, files in os.walk(p):
dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS)
for f in sorted(files):
if wanted(f):
yield os.path.join(root, f)
def scan(path):
out = []
name = os.path.basename(path)
try:
with open(path, encoding="utf-8", errors="replace") as fh:
lines = fh.read().splitlines()
except OSError as e:
return [(path, 0, "error", "unreadable", str(e))]
for n, raw in enumerate(lines, 1):
code = strip_comment(raw)
if not code.strip():
continue
accepted = "guard: ok" in raw # a person accepted the warnings on this line
for rid, lvl, rx, msg, globs in COMPILED:
if accepted and lvl == "warn":
continue
if globs and not any(fnmatch.fnmatch(name, g) for g in globs):
continue
if rx.search(code):
out.append((path, n, lvl, rid, msg))
return out
def main(argv):
if "--list" in argv:
for rid, lvl, _, msg, globs in RULES:
print(f"{lvl:5} [{rid}] {msg}" + (f" (only {', '.join(globs)})" if globs else ""))
return 0
if any(a.startswith("-") for a in argv):
print(__doc__)
return 2
paths = argv or ["."]
for p in paths:
if not os.path.exists(p):
print(f"no such path: {p}", file=sys.stderr)
return 2
files = list(walk(paths))
findings = []
for f in files:
findings.extend(scan(f))
for path, n, lvl, rid, msg in findings:
print(f"{os.path.relpath(path)}:{n}: {lvl} [{rid}] {msg}")
errors = sum(1 for x in findings if x[2] == "error")
warns = sum(1 for x in findings if x[2] == "warn")
print(f"pandas3_guard: {len(files)} file(s), {errors} error(s), {warns} warning(s)")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:])) Skill file skills/pandas-3-guard/references/rename-table.md pandas 1 and 2 to pandas 3 table, including copy-on-write fixes. 51 lines
# pandas 1/2 → pandas 3 table
From the pandas 3.0.0 release notes (https://pandas.pydata.org/docs/whatsnew/v3.0.0.html) and the copy-on-write
guide (https://pandas.pydata.org/docs/user_guide/copy_on_write.html). Checked against pandas 3.1.0.
## Removed: raises in pandas 3
| Old | pandas 3 |
|---|---|
| `df.applymap(f)` | `df.map(f)` |
| `df.append(other)` (removed in 2.0) | `pd.concat([df, other], ignore_index=True)` |
| `df.iteritems()` (removed in 2.0) | `df.items()` |
| `df.fillna(method="ffill")` | `df.ffill()` |
| `df.replace(x, method="pad")` | `df.replace(x, np.nan).ffill()` |
| `s.interpolate("pad")` | `s.ffill()` |
| `freq="H"`, `"BH"`, `"T"`, `"L"`, `"U"`, `"N"` | `"h"`, `"bh"`, `"min"`, `"ms"`, `"us"`, `"ns"` |
| `freq="M"`, `"Q"`, `"Y"`, `"A"` (ends) | `"ME"`, `"QE"`, `"YE"` |
| `read_csv(delim_whitespace=True)` | `read_csv(sep=r"\s+")` |
| `read_csv(date_parser=f)` | `read_csv(date_format="...")` or `pd.to_datetime` afterwards |
| `read_csv(infer_datetime_format=True)` | drop it; strict inference is the default |
| `df.swapaxes(0, 1)` | `df.T` |
| `s.view("int64")` | `s.astype("int64")` |
| `df.groupby("k", axis=1)` | `df.T.groupby("k")` |
| `df.first("3D")` / `df.last("3D")` | `df.loc[df.index < df.index[0] + pd.Timedelta("3D")]` |
| `pd.to_numeric(s, errors="ignore")` | `pd.to_numeric(s, errors="coerce")` |
| `s.bool()` | `bool(s.item())` |
| `pd.set_option("mode.use_inf_as_na", True)` | `df.replace([np.inf, -np.inf], np.nan)` |
| `pd.read_json('[{"a": 1}]')` | `pd.read_json(io.StringIO(text))` (same for read_html, read_xml) |
## Copy-on-write: writes that are silently lost
| Old | pandas 3 |
|---|---|
| `df["v"][0] = 1` | `df.loc[0, "v"] = 1` |
| `df[df.a > 0]["b"] = 0` | `df.loc[df.a > 0, "b"] = 0` |
| `df["v"].fillna(0, inplace=True)` | `df["v"] = df["v"].fillna(0)` |
| `sub = df[cols]; sub["x"] = 1` expecting `df` to change | change `df` itself |
| defensive `.copy()` to silence SettingWithCopyWarning | not needed; the warning is gone |
## Changed meaning: same code, different answer
| Change | What to do |
|---|---|
| String columns are dtype `str`, not `object` | `pd.api.types.is_string_dtype(s)`; `select_dtypes(include=["str"])` |
| A `str` column refuses non-strings | convert on purpose if mixed values are wanted |
| Missing strings are `NaN` | test with `isna()`, not `is None` |
| `groupby(...)` on categoricals defaults to `observed=True` | pass `observed=False` to keep empty categories |
| `agg(np.sum)` is no longer swapped for pandas' sum | `agg("sum")` |
| `Series[int]` always means a label, never a position | `s.iloc[i]` for positions |
| `mode.copy_on_write` option does nothing | remove it | Skill file skills/pandas-3-guard/references/pandas2-sample.py.txt A pandas 2 cleaning script for checking the guard. 17 lines
# A pandas 2 cleaning script, used to check the guard. Every marked line fails or silently changes in pandas 3.
import numpy as np
import pandas as pd
df = pd.read_csv("readings.txt", delim_whitespace=True) # delim-whitespace
df = pd.read_csv("readings.csv", parse_dates=["day"], date_parser=pd.to_datetime) # date-parser
df = df.applymap(lambda v: v.strip() if isinstance(v, str) else v) # applymap
df = df.fillna(method="ffill") # fillna-method
df["reading"][0] = 0.0 # chained-assignment
df["site"].fillna("unknown", inplace=True) # inplace-on-column
hourly = df.set_index("day").resample("H").mean() # resample-alias
months = pd.date_range("2026-01-01", periods=12, freq="M") # freq-alias
codes = pd.to_numeric(df["code"], errors="ignore") # errors-ignore
more = df.append(df.tail(1)) # append
text_cols = [c for c in df.columns if df[c].dtype == object] # object-dtype-check
totals = df.groupby("site").agg(np.sum) # np-reduction-in-agg All files: source zip · Licence MIT · By SetupBay · First listed
Credits
Public work that inspired this loadout. The wording here is our own; follow the links for the originals.
Installing and checks
How do I install pandas 3 guard for copy-on-write and removed APIs in Claude Code?
From your project root, put AGENTS.md, CLAUDE.md, skills/pandas-3-guard/SKILL.md at .claude/skills/pandas-3-guard/SKILL.md, skills/pandas-3-guard/scripts/pandas3_guard.py at .claude/skills/pandas-3-guard/scripts/pandas3_guard.py, skills/pandas-3-guard/references/rename-table.md at .claude/skills/pandas-3-guard/references/rename-table.md, skills/pandas-3-guard/references/pandas2-sample.py.txt at .claude/skills/pandas-3-guard/references/pandas2-sample.py.txt. The "Claude Code" tab on this page has the curl commands, or download https://setupbay.com/download/pandas-3-upgrade-guard/claude-code.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install pandas 3 guard for copy-on-write and removed APIs in Codex CLI?
From your project root, put AGENTS.md, skills/pandas-3-guard/SKILL.md at .agents/skills/pandas-3-guard/SKILL.md, skills/pandas-3-guard/scripts/pandas3_guard.py at .agents/skills/pandas-3-guard/scripts/pandas3_guard.py, skills/pandas-3-guard/references/rename-table.md at .agents/skills/pandas-3-guard/references/rename-table.md, skills/pandas-3-guard/references/pandas2-sample.py.txt at .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt. The "Codex CLI" tab on this page has the curl commands, or download https://setupbay.com/download/pandas-3-upgrade-guard/codex.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install pandas 3 guard for copy-on-write and removed APIs in Cursor?
From your project root, put AGENTS.md, .cursor/rules/pandas-3-only.mdc, skills/pandas-3-guard/SKILL.md at .agents/skills/pandas-3-guard/SKILL.md, skills/pandas-3-guard/scripts/pandas3_guard.py at .agents/skills/pandas-3-guard/scripts/pandas3_guard.py, skills/pandas-3-guard/references/rename-table.md at .agents/skills/pandas-3-guard/references/rename-table.md, skills/pandas-3-guard/references/pandas2-sample.py.txt at .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt. The "Cursor" tab on this page has the curl commands, or download https://setupbay.com/download/pandas-3-upgrade-guard/cursor.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install pandas 3 guard for copy-on-write and removed APIs in Gemini CLI?
From your project root, put AGENTS.md, GEMINI.md, skills/pandas-3-guard/SKILL.md at .agents/skills/pandas-3-guard/SKILL.md, skills/pandas-3-guard/scripts/pandas3_guard.py at .agents/skills/pandas-3-guard/scripts/pandas3_guard.py, skills/pandas-3-guard/references/rename-table.md at .agents/skills/pandas-3-guard/references/rename-table.md, skills/pandas-3-guard/references/pandas2-sample.py.txt at .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt. The "Gemini CLI" tab on this page has the curl commands, or download https://setupbay.com/download/pandas-3-upgrade-guard/gemini-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install pandas 3 guard for copy-on-write and removed APIs in Antigravity CLI?
From your project root, put AGENTS.md, skills/pandas-3-guard/SKILL.md at .agents/skills/pandas-3-guard/SKILL.md, skills/pandas-3-guard/scripts/pandas3_guard.py at .agents/skills/pandas-3-guard/scripts/pandas3_guard.py, skills/pandas-3-guard/references/rename-table.md at .agents/skills/pandas-3-guard/references/rename-table.md, skills/pandas-3-guard/references/pandas2-sample.py.txt at .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt. The "Antigravity CLI" tab on this page has the curl commands, or download https://setupbay.com/download/pandas-3-upgrade-guard/antigravity-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install pandas 3 guard for copy-on-write and removed APIs in GitHub Copilot?
From your project root, put AGENTS.md, skills/pandas-3-guard/SKILL.md at .agents/skills/pandas-3-guard/SKILL.md, skills/pandas-3-guard/scripts/pandas3_guard.py at .agents/skills/pandas-3-guard/scripts/pandas3_guard.py, skills/pandas-3-guard/references/rename-table.md at .agents/skills/pandas-3-guard/references/rename-table.md, skills/pandas-3-guard/references/pandas2-sample.py.txt at .agents/skills/pandas-3-guard/references/pandas2-sample.py.txt. The "GitHub Copilot" tab on this page has the curl commands, or download https://setupbay.com/download/pandas-3-upgrade-guard/copilot.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
What has been checked in pandas 3 guard for copy-on-write and removed APIs?
It passed the Level 1 automated install checks on 2026-10-29: the files parse, install to each agent's documented paths, and pass the safety scan. No AI agent has run it yet, so no agent pass or fail is claimed.
More for Claude Code, Codex, Cursor, Gemini CLI, Antigravity CLI and GitHub Copilot.