Python Project Best Practice
An opinionated baseline for Python projects that both people and agents work
in. Two entry points: scaffold a new project (Workflow A), or score and
modernize an existing one (Workflow B). The same conventions drive both.
The bias throughout: every operation has one canonical, discoverable,
non-interactive command. That is what makes a project workable by an agent,
and it happens to be what makes it pleasant for humans.
When to use
- "Start a new Python project / package / CLI tool"
- "Set up pyproject.toml", "which should I use, uv or poetry / pip-tools?"
- "How do I make this installable so someone can just run it?"
- "Modernize this repo" — setup.py, requirements.txt, poetry, conda, flat layout
- Choosing between ruff/black, mypy/ty, extras/dependency-groups, just/taskipy
- Adding a CLI, notebooks, an HTTP surface, or a Rust extension to a Python repo
When NOT to use
These skills own their territory. Delegate rather than restate:
| Topic |
Skill |
--help / --dry-run / --print-config / exit-code design and verification |
verifiable-surfaces |
| marimo dual-mode notebooks, sweeps, tracking |
marimo-batch-mlflow; format rules in marimo-notebook; widgets in anywidget |
| Production FastAPI internals |
fastapi-ai-patterns, fastapi-ai-scaffold |
| Docs site, GitHub Pages, llms.txt |
mkdocs-site-bootstrap |
| Releasing a compiled Go/Rust binary |
cli-release-distribution (Python distribution stays here) |
| pre-commit setup, secret remediation |
agent-history-hygiene |
| TDD discipline itself |
engineering-fundamentals/tdd |
| Building an MCP server |
mcp-builder (read references/api-and-services.md first — usually you want a subcommand) |
Also not for: a one-file script (use a PEP 723 header and stop), or a
maintenance-only repo where a migration buys nothing.
Authoritative sources
Fetch the page rather than guessing — this toolchain moves fast, and several
of these projects change CLI surface between minor versions.
The canonical layout
pyproject.toml [project] + [dependency-groups] + tool config. One file.
uv.lock committed. Pins YOUR dev env, never your users'.
.python-version dev interpreter (uv python pin). Not the same as requires-python.
Justfile every supported operation. `just --list` is the surface.
AGENTS.md the agent contract. CLAUDE.md is a symlink to it.
README.md getting started for humans: uv sync, just check.
.envrc / .env.example direnv activation; the tracked contract for .env (which is ignored).
src/my_tool/ src layout: tests import the INSTALLED package
core.py domain logic. stdlib logging. no side effects at import.
_log.py loguru setup. called by entry points only.
settings.py pydantic-settings: defaults < .env < environment.
cli/ one frozen dataclass per subcommand; run() -> exit code
py.typed without this your annotations are invisible to consumers
tests/ mirrors src/ module for module
scripts/ repo helpers, NOT importable from the package
notebooks/ marimo notebooks. import the package; nothing imports them.
.agents/skills/my-tool/ the package's own skill, shipped with the code
.github/workflows/ci.yml runs the same gate as `just check`
Two arrows to keep straight: notebooks/ and scripts/ may import the
package; the package imports neither.
Decision: which profile?
Start at the smallest row that fits and add later — profiles are additive, so
"agentic later" costs nothing now.
| Profile |
Use when |
Adds |
Read |
minimal |
a library nobody runs from a shell yet |
package, tests, Justfile, CI, AGENTS.md |
uv-and-pyproject, quality-gates |
cli (default) |
almost everything |
Tyro CLI, loguru, pydantic-settings, [project.scripts] |
+ tyro-cli, logging-and-config |
lib |
others will import it |
publishing metadata, py.typed |
+ uv-and-pyproject §Distribution |
api |
it serves HTTP |
FastAPI app, /docs, /openapi.json |
+ api-and-services |
ml |
experiments and notebooks |
marimo notebook that is also a CLI |
+ notebooks-and-widgets |
rust |
a profiled, CPU-bound hot loop |
maturin backend, PyO3 crate, .pyi stubs |
+ rust-pyo3 |
Companion skills to recommend, by profile, are in
references/agent-interface.md and in the
scaffolder's recommended_skills[].
Workflow A — new project from zero
1. Decide the profile and the name
Ask only what you cannot infer: what the project does, and whether it needs a
CLI, HTTP, notebooks, or Rust. The slug is hyphen-case (churn-scorer); the
package is the underscore form (churn_scorer). Everything else has a default.
2. Generate
# preview first — writes nothing
uv run skills/local/python-project-best-practice/scripts/new-python-project.py \
--dry-run --profile cli ./my-tool
uv run .../new-python-project.py --profile cli --owner <gh-user> \
--description "One line." ./my-tool
JSON summary on stdout (project, package, profile, files[],
recommended_skills[], next_steps[]); progress on stderr.
3. Verify it actually works
cd ./my-tool
uv sync
just docs-sync # fills the CLI block in AGENTS.md from --help
just check # fmt + lint + types + tests + docs-drift
just check must be green before you hand the project over. If it is not,
that is a bug in this skill's template — fix the template, not the generated
copy.
4. Offer the companion skills
Print the install line and let the user choose:
npx skills@latest add daviddwlee84/agent-skills/skills
Do not vendor copies into the new repo — copies have no update path.
Workflow B — modernize an existing project
1. Audit, read-only
uv run skills/local/python-project-best-practice/scripts/audit-python-project.py \
--format table /path/to/repo
26 checks with evidence and a fix hint, plus an ordered migration_plan[].
Exit 4 when something fails. It never writes, and there is deliberately no
--fix.
2. Show the user the plan and agree on scope
Nobody asked you to rewrite their build system. Confirm which rungs to do now.
3. One rung per pull request
The ladder is ordered by dependency: environment → layout → dependency
declaration → lint/format → types → tests → task runner + CI → console script
→ logging layering → agent contract → secrets. Each rung ends with the test
suite green.
Read references/legacy-refactor.md before
the first rung. The traps that ruin these migrations — uv add -r requirements.txt flattening the dependency graph, a formatting commit
destroying git blame, a tracked .env needing rotation before any history
rewrite — are all there.
Available scripts
scripts/new-python-project.py <target-dir> — scaffold from the bundled
template tree. Offline; writes nothing outside the target.
- Flags:
--profile {minimal,cli,lib,api,ml,rust}, --name, --description,
--author, --owner, --python-floor, --python-pin, --dry-run,
--force, --no-git, --help.
- Output: JSON on stdout, progress on stderr.
- Exit:
0 ok · 2 usage · 3 target exists without --force ·
4 template tree and assets/manifest.toml disagree.
scripts/audit-python-project.py [path] — read-only scorecard +
migration plan.
- Flags:
--format {json,table}, --fail-on {fail,warn,never}, --help.
- Exit:
0 clean · 3 path is not a directory · 4 findings at or above
the threshold.
Both are PEP 723 uv run scripts with inline dependencies — no environment
setup needed before calling them.
Bundled assets
assets/project/ — the template tree. Every file ends .tmpl; the generator
strips the suffix, substitutes placeholders, and drops
# __IF:profile,...__ … # __END__ blocks that the chosen profile does not
want.
assets/manifest.toml — maps each destination path to the profiles that get
it. A file not listed here is never copied, and the generator fails
(exit 4) if the manifest and the tree disagree in either direction.
Reference files
Load on demand — do not read them all up front.
| File |
Load when |
uv-and-pyproject.md |
writing pyproject.toml, lockfile policy, build backends, interpreter pinning, workspaces, publishing |
tyro-cli.md |
building or splitting a CLI, subcommands, completion, config objects |
quality-gates.md |
configuring ruff, a type checker, pytest, coverage, pre-commit, CI |
logging-and-config.md |
wiring loguru, pydantic-settings, .env, direnv |
notebooks-and-widgets.md |
adding notebooks/, dual-mode notebooks, packaged widgets |
api-and-services.md |
the project serves HTTP, or someone asks "should this be an MCP?" |
rust-pyo3.md |
adding a compiled extension, or a Rust edit appears to do nothing |
agent-interface.md |
writing AGENTS.md, the package's own skill, the docs-drift gate |
legacy-refactor.md |
Workflow B — reading the scorecard, ordering the migration |
Gotchas
uv sync deletes what is not in the lock. A manual uv pip install
disappears on the next sync with no warning. Change dependencies with
uv add / uv remove so pyproject.toml and uv.lock move together.
- Do not run black and
ruff format together. ruff format is a black
reimplementation; two formatters fight over magic trailing commas and quoting
and every commit reformats the last one's output. Pick ruff.
- A flat layout imports your source directory, not the installed package.
Missing
__init__.py, missing py.typed, unshipped package data — all pass
the test suite and break the user's install. src/ is what closes that gap.
[dependency-groups] is not [project.optional-dependencies]. An extra
is published and installable by your users; a PEP 735 group is local-only.
Your test runner in an extra is a defect you cannot remove without a release.
uv sync installs the dev group by default.
uv.lock does not constrain your users. Installed as a dependency, your
package is resolved from [project.dependencies] and the lock is ignored.
Commit it anyway — it pins your dev and CI environment, which is the point.
- loguru in a library hijacks the importing program's logging. Library
modules use
logging.getLogger(__name__); only entry points call
_log.configure(). Symptom: "my app's log format changed after I added a
dependency."
logger.add() without logger.remove() prints everything twice. loguru
installs a default stderr sink at import. This is the most common loguru bug.
diagnose=True puts local variables in tracebacks — a credential leak in
production logs. Keep it off outside development.
.python-version is read by pyenv as well as uv. A stale value silently
changes which interpreter builds the venv. Write it with uv python pin.
It is the dev pin; requires-python is the supported range. Keep both in CI.
ruff's target-version should be your requires-python floor, not
your dev pin, or UP rewrites code to syntax your declared floor cannot run.
- direnv cannot change
PS1. There is no (.venv) prompt even when the
env is active — check which python, not the prompt. And .envrc needs
direnv allow after every edit.
- Tyro already has shell completion. Do not add shtab, argcomplete, or a
completion subcommand: my-tool --tyro-write-completion zsh <path>.
--tyro-print-completion is deprecated (a stray print() corrupts it), the
bash completion filename must equal the command name, and no package manager
installs completions for you.
- No
[project.scripts] means uv tool install gives the user no command.
python -m pkg is not a substitute for something on PATH.
- marimo notebooks are real
.py files, so ruff lints them: without
per-file-ignores for notebooks/* (E402, F401, B018) the gate fails
and the formatter can reorder cell contents.
- A PEP 723 script runs in its own environment. A helper in
scripts/
with an inline header cannot import your_package unless the package is in
its own dependencies.
uv sync does not rebuild a Rust extension after you edit rust/. It
sees the package as installed, does nothing, and you keep running the stale
binary — including the print statements you just added to debug it. Use
uv sync --reinstall-package <slug> or maturin develop --uv. A compiled
module also needs a hand-written .pyi, or type checkers report
has no member.
- Never replace the
AGENTS.md↔CLAUDE.md symlink with a real file. Git
stores it as mode 120000; two real files drift and only one is right. A
Windows checkout without symlink support materializes a text file containing
the path — that is the corruption, not the fix.
- Do not
uv add -r requirements.txt when migrating. A pip freeze is a
flattened graph; that command promotes every transitive pin to a direct
dependency. Add the packages you actually import and let the resolver work.
- uv does not supply non-Python system libraries. Some scientific stacks
still need conda or OS packages. Say which ones in the README instead of
pretending the migration is complete.
- A tracked
.env is an incident, not a cleanup. Rotate every credential
first; history rewriting is a later, separately-approved step. Hand off to
agent-history-hygiene.
Related skills
verifiable-surfaces (CLI surface design — assumed by this layout) ·
project-knowledge-harness · agent-history-hygiene ·
mkdocs-site-bootstrap · git-workflow · marimo-batch-mlflow ·
fastapi-ai-patterns · cli-release-distribution · mcp-builder
1---2name: python-project-best-practice3description: Modern Python project conventions for the agentic-coding era: uv + src layout, Tyro CLIs with shell completion, marimo notebooks that run as scripts, loguru, and ruff/type/pytest gates behind a Justfile, plus an AGENTS.md contract agents can drive. Use when starting a new Python project or package, scaffolding a pyproject.toml, choosing a CLI/test/lint/logging stack, making a tool installable with `uv tool install`, or modernizing a legacy setup.py / requirements.txt / poetry / conda repo.4---56# Python Project Best Practice78An opinionated baseline for Python projects that both people and agents work9in. Two entry points: scaffold a new project (Workflow A), or score and10modernize an existing one (Workflow B). The same conventions drive both.1112The bias throughout: **every operation has one canonical, discoverable,13non-interactive command**. That is what makes a project workable by an agent,14and it happens to be what makes it pleasant for humans.1516## When to use1718- "Start a new Python project / package / CLI tool"19- "Set up pyproject.toml", "which should I use, uv or poetry / pip-tools?"20- "How do I make this installable so someone can just run it?"21- "Modernize this repo" — setup.py, requirements.txt, poetry, conda, flat layout22- Choosing between ruff/black, mypy/ty, extras/dependency-groups, just/taskipy23- Adding a CLI, notebooks, an HTTP surface, or a Rust extension to a Python repo2425## When NOT to use2627These skills own their territory. Delegate rather than restate:2829| Topic | Skill |30|---|---|31| `--help` / `--dry-run` / `--print-config` / exit-code design and verification | `verifiable-surfaces` |32| marimo dual-mode notebooks, sweeps, tracking | `marimo-batch-mlflow`; format rules in `marimo-notebook`; widgets in `anywidget` |33| Production FastAPI internals | `fastapi-ai-patterns`, `fastapi-ai-scaffold` |34| Docs site, GitHub Pages, llms.txt | `mkdocs-site-bootstrap` |35| Releasing a compiled Go/Rust binary | `cli-release-distribution` (Python distribution stays here) |36| pre-commit setup, secret remediation | `agent-history-hygiene` |37| TDD discipline itself | `engineering-fundamentals/tdd` |38| Building an MCP server | `mcp-builder` (read `references/api-and-services.md` first — usually you want a subcommand) |3940Also not for: a one-file script (use a PEP 723 header and stop), or a41maintenance-only repo where a migration buys nothing.4243## Authoritative sources4445Fetch the page rather than guessing — this toolchain moves fast, and several46of these projects change CLI surface between minor versions.4748| Thing | Where |49|---|---|50| uv | <https://docs.astral.sh/uv/> · CLI reference <https://docs.astral.sh/uv/reference/cli/> |51| ruff rules | <https://docs.astral.sh/ruff/rules/> |52| ty | <https://github.com/astral-sh/ty> (0.0.x — check the version before quoting behavior) |53| Tyro | <https://brentyi.github.io/tyro/> · completion <https://brentyi.github.io/tyro/tab_completion/> |54| loguru | <https://loguru.readthedocs.io/> |55| pydantic-settings | <https://docs.pydantic.dev/latest/concepts/pydantic_settings/> |56| PyO3 / maturin | <https://pyo3.rs/> · <https://www.maturin.rs/> |57| Packaging specs | <https://packaging.python.org/> · PEP 735 (dependency groups), PEP 723 (script deps) |5859## The canonical layout6061```62pyproject.toml [project] + [dependency-groups] + tool config. One file.63uv.lock committed. Pins YOUR dev env, never your users'.64.python-version dev interpreter (uv python pin). Not the same as requires-python.65Justfile every supported operation. `just --list` is the surface.66AGENTS.md the agent contract. CLAUDE.md is a symlink to it.67README.md getting started for humans: uv sync, just check.68.envrc / .env.example direnv activation; the tracked contract for .env (which is ignored).6970src/my_tool/ src layout: tests import the INSTALLED package71 core.py domain logic. stdlib logging. no side effects at import.72 _log.py loguru setup. called by entry points only.73 settings.py pydantic-settings: defaults < .env < environment.74 cli/ one frozen dataclass per subcommand; run() -> exit code75 py.typed without this your annotations are invisible to consumers76tests/ mirrors src/ module for module77scripts/ repo helpers, NOT importable from the package78notebooks/ marimo notebooks. import the package; nothing imports them.79.agents/skills/my-tool/ the package's own skill, shipped with the code80.github/workflows/ci.yml runs the same gate as `just check`81```8283Two arrows to keep straight: `notebooks/` and `scripts/` may import the84package; the package imports neither.8586## Decision: which profile?8788Start at the smallest row that fits and add later — profiles are additive, so89"agentic later" costs nothing now.9091| Profile | Use when | Adds | Read |92|---|---|---|---|93| `minimal` | a library nobody runs from a shell yet | package, tests, Justfile, CI, AGENTS.md | `uv-and-pyproject`, `quality-gates` |94| `cli` **(default)** | almost everything | Tyro CLI, loguru, pydantic-settings, `[project.scripts]` | + `tyro-cli`, `logging-and-config` |95| `lib` | others will import it | publishing metadata, `py.typed` | + `uv-and-pyproject` §Distribution |96| `api` | it serves HTTP | FastAPI app, `/docs`, `/openapi.json` | + `api-and-services` |97| `ml` | experiments and notebooks | marimo notebook that is also a CLI | + `notebooks-and-widgets` |98| `rust` | a profiled, CPU-bound hot loop | maturin backend, PyO3 crate, `.pyi` stubs | + `rust-pyo3` |99100Companion skills to recommend, by profile, are in101[`references/agent-interface.md`](references/agent-interface.md) and in the102scaffolder's `recommended_skills[]`.103104## Workflow A — new project from zero105106### 1. Decide the profile and the name107108Ask only what you cannot infer: what the project does, and whether it needs a109CLI, HTTP, notebooks, or Rust. The slug is hyphen-case (`churn-scorer`); the110package is the underscore form (`churn_scorer`). Everything else has a default.111112### 2. Generate113114```bash115# preview first — writes nothing116uv run skills/local/python-project-best-practice/scripts/new-python-project.py \117 --dry-run --profile cli ./my-tool118119uv run .../new-python-project.py --profile cli --owner <gh-user> \120 --description "One line." ./my-tool121```122123JSON summary on stdout (`project`, `package`, `profile`, `files[]`,124`recommended_skills[]`, `next_steps[]`); progress on stderr.125126### 3. Verify it actually works127128```bash129cd ./my-tool130uv sync131just docs-sync # fills the CLI block in AGENTS.md from --help132just check # fmt + lint + types + tests + docs-drift133```134135`just check` must be green before you hand the project over. If it is not,136that is a bug in this skill's template — fix the template, not the generated137copy.138139### 4. Offer the companion skills140141Print the install line and let the user choose:142143```bash144npx skills@latest add daviddwlee84/agent-skills/skills145```146147Do not vendor copies into the new repo — copies have no update path.148149## Workflow B — modernize an existing project150151### 1. Audit, read-only152153```bash154uv run skills/local/python-project-best-practice/scripts/audit-python-project.py \155 --format table /path/to/repo156```15715826 checks with evidence and a fix hint, plus an ordered `migration_plan[]`.159Exit `4` when something fails. It never writes, and there is deliberately no160`--fix`.161162### 2. Show the user the plan and agree on scope163164Nobody asked you to rewrite their build system. Confirm which rungs to do now.165166### 3. One rung per pull request167168The ladder is ordered by dependency: environment → layout → dependency169declaration → lint/format → types → tests → task runner + CI → console script170→ logging layering → agent contract → secrets. Each rung ends with the test171suite green.172173Read [`references/legacy-refactor.md`](references/legacy-refactor.md) before174the first rung. The traps that ruin these migrations — `uv add -r175requirements.txt` flattening the dependency graph, a formatting commit176destroying `git blame`, a tracked `.env` needing rotation before any history177rewrite — are all there.178179## Available scripts180181- **`scripts/new-python-project.py <target-dir>`** — scaffold from the bundled182 template tree. Offline; writes nothing outside the target.183 - Flags: `--profile {minimal,cli,lib,api,ml,rust}`, `--name`, `--description`,184 `--author`, `--owner`, `--python-floor`, `--python-pin`, `--dry-run`,185 `--force`, `--no-git`, `--help`.186 - Output: JSON on stdout, progress on stderr.187 - Exit: `0` ok · `2` usage · `3` target exists without `--force` ·188 `4` template tree and `assets/manifest.toml` disagree.189- **`scripts/audit-python-project.py [path]`** — read-only scorecard +190 migration plan.191 - Flags: `--format {json,table}`, `--fail-on {fail,warn,never}`, `--help`.192 - Exit: `0` clean · `3` path is not a directory · `4` findings at or above193 the threshold.194195Both are PEP 723 `uv run` scripts with inline dependencies — no environment196setup needed before calling them.197198## Bundled assets199200- `assets/project/` — the template tree. Every file ends `.tmpl`; the generator201 strips the suffix, substitutes placeholders, and drops202 `# __IF:profile,...__ … # __END__` blocks that the chosen profile does not203 want.204- `assets/manifest.toml` — maps each destination path to the profiles that get205 it. **A file not listed here is never copied**, and the generator fails206 (exit 4) if the manifest and the tree disagree in either direction.207208## Reference files209210Load on demand — do not read them all up front.211212| File | Load when |213|---|---|214| [`uv-and-pyproject.md`](references/uv-and-pyproject.md) | writing `pyproject.toml`, lockfile policy, build backends, interpreter pinning, workspaces, publishing |215| [`tyro-cli.md`](references/tyro-cli.md) | building or splitting a CLI, subcommands, completion, config objects |216| [`quality-gates.md`](references/quality-gates.md) | configuring ruff, a type checker, pytest, coverage, pre-commit, CI |217| [`logging-and-config.md`](references/logging-and-config.md) | wiring loguru, pydantic-settings, `.env`, direnv |218| [`notebooks-and-widgets.md`](references/notebooks-and-widgets.md) | adding `notebooks/`, dual-mode notebooks, packaged widgets |219| [`api-and-services.md`](references/api-and-services.md) | the project serves HTTP, or someone asks "should this be an MCP?" |220| [`rust-pyo3.md`](references/rust-pyo3.md) | adding a compiled extension, or a Rust edit appears to do nothing |221| [`agent-interface.md`](references/agent-interface.md) | writing AGENTS.md, the package's own skill, the docs-drift gate |222| [`legacy-refactor.md`](references/legacy-refactor.md) | Workflow B — reading the scorecard, ordering the migration |223224## Gotchas225226- **`uv sync` deletes what is not in the lock.** A manual `uv pip install`227 disappears on the next sync with no warning. Change dependencies with228 `uv add` / `uv remove` so `pyproject.toml` and `uv.lock` move together.229- **Do not run black and `ruff format` together.** `ruff format` is a black230 reimplementation; two formatters fight over magic trailing commas and quoting231 and every commit reformats the last one's output. Pick ruff.232- **A flat layout imports your source directory, not the installed package.**233 Missing `__init__.py`, missing `py.typed`, unshipped package data — all pass234 the test suite and break the user's install. `src/` is what closes that gap.235- **`[dependency-groups]` is not `[project.optional-dependencies]`.** An extra236 is published and installable by your users; a PEP 735 group is local-only.237 Your test runner in an extra is a defect you cannot remove without a release.238 `uv sync` installs the `dev` group by default.239- **`uv.lock` does not constrain your users.** Installed as a dependency, your240 package is resolved from `[project.dependencies]` and the lock is ignored.241 Commit it anyway — it pins your dev and CI environment, which is the point.242- **loguru in a library hijacks the importing program's logging.** Library243 modules use `logging.getLogger(__name__)`; only entry points call244 `_log.configure()`. Symptom: "my app's log format changed after I added a245 dependency."246- **`logger.add()` without `logger.remove()` prints everything twice.** loguru247 installs a default stderr sink at import. This is the most common loguru bug.248- **`diagnose=True` puts local variables in tracebacks** — a credential leak in249 production logs. Keep it off outside development.250- **`.python-version` is read by pyenv as well as uv.** A stale value silently251 changes which interpreter builds the venv. Write it with `uv python pin`.252 It is the dev pin; `requires-python` is the supported range. Keep both in CI.253- **`ruff`'s `target-version` should be your `requires-python` floor**, not254 your dev pin, or `UP` rewrites code to syntax your declared floor cannot run.255- **direnv cannot change `PS1`.** There is no `(.venv)` prompt even when the256 env is active — check `which python`, not the prompt. And `.envrc` needs257 `direnv allow` after every edit.258- **Tyro already has shell completion.** Do not add shtab, argcomplete, or a259 `completion` subcommand: `my-tool --tyro-write-completion zsh <path>`.260 `--tyro-print-completion` is deprecated (a stray `print()` corrupts it), the261 bash completion filename must equal the command name, and no package manager262 installs completions for you.263- **No `[project.scripts]` means `uv tool install` gives the user no command.**264 `python -m pkg` is not a substitute for something on `PATH`.265- **marimo notebooks are real `.py` files**, so ruff lints them: without266 `per-file-ignores` for `notebooks/*` (`E402`, `F401`, `B018`) the gate fails267 and the formatter can reorder cell contents.268- **A PEP 723 script runs in its own environment.** A helper in `scripts/`269 with an inline header cannot `import your_package` unless the package is in270 its own `dependencies`.271- **`uv sync` does not rebuild a Rust extension after you edit `rust/`.** It272 sees the package as installed, does nothing, and you keep running the stale273 binary — including the print statements you just added to debug it. Use274 `uv sync --reinstall-package <slug>` or `maturin develop --uv`. A compiled275 module also needs a hand-written `.pyi`, or type checkers report276 `has no member`.277- **Never replace the `AGENTS.md`↔`CLAUDE.md` symlink with a real file.** Git278 stores it as mode `120000`; two real files drift and only one is right. A279 Windows checkout without symlink support materializes a text file containing280 the path — that is the corruption, not the fix.281- **Do not `uv add -r requirements.txt` when migrating.** A pip freeze is a282 flattened graph; that command promotes every transitive pin to a direct283 dependency. Add the packages you actually import and let the resolver work.284- **uv does not supply non-Python system libraries.** Some scientific stacks285 still need conda or OS packages. Say which ones in the README instead of286 pretending the migration is complete.287- **A tracked `.env` is an incident, not a cleanup.** Rotate every credential288 first; history rewriting is a later, separately-approved step. Hand off to289 `agent-history-hygiene`.290291## Related skills292293`verifiable-surfaces` (CLI surface design — assumed by this layout) ·294`project-knowledge-harness` · `agent-history-hygiene` ·295`mkdocs-site-bootstrap` · `git-workflow` · `marimo-batch-mlflow` ·296`fastapi-ai-patterns` · `cli-release-distribution` · `mcp-builder`