Use when creating or modifying standalone Python scripts, CLIs, or small operator-facing toolkits whose primary contract is direct execution rather than reusable package or application code.
Follow .github/instructions/internal-python.instructions.md for the baseline Python rules. This skill adds standalone-script guidance only.
lib/ folder, or root-level tests..py file.main() -> int plus raise SystemExit(main()).argparse, pathlib.Path, and small helper functions for operator-facing tools.asyncio only when the script truly coordinates multiple I/O-bound tasks.pathlib, context managers, and small helper functions before adding framework-like structure to a script.--format json only when the tool has a real automation consumer. Keep text output as the default operator path.When the instruction owner requires a dependency decision note, keep it short, for example:
Dependency decision note
- Candidates: argparse (stdlib), click, typer
- Final choice: typer
- Why: cleaner CLI structure, less boilerplate, better help output, and less custom parsing code than argparse for this script.
requirements.txt before finishing the task.requirements.txt rather than repeating it in every script.Load references/layout-and-templates.md when you need the default folder layout, a repo-aligned multi-tool toolkit layout, a minimal entry point, a hash-locked requirements.txt, or the launcher pattern.
Keep these rules visible while drafting:
.py file.requirements.txt and run.sh only when external packages are actually needed.requirements.txt with pip-compile --generate-hashes or an equivalent locked workflow..github/scripts/run.sh instead of cloning bootstrap logic into every entrypoint.tests/ tree; do not create ad-hoc test folders beside the tool.make lint, make test, or a shared script runner before inventing a one-off validation path..venv and pip install logic into every wrapper.| Mistake | Why it matters | Instead |
|---|---|---|
Missing if __name__ == "__main__": guard | Script runs on import, breaks testing and reuse | Always guard the entry point |
Using print() for errors | Errors go to stdout, mixed with normal output | Use print(..., file=sys.stderr) or logging |
Bare except: or except Exception: at top level | Swallows all errors including KeyboardInterrupt | Catch specific exceptions; let unexpected ones propagate |
| Hardcoded file paths | Non-portable across machines | Use argparse, pathlib, or environment variables |
| No argument parsing | Caller has to modify script source to change behavior | Use argparse for any configurable parameter |
| Installing deps globally or without hash-locked version pinning | Non-reproducible environment and hidden setup drift | Keep dependencies in the local requirements.txt with exact pins and hashes |
Adding an empty requirements.txt to a stdlib-only tool | Adds noise and implies missing setup steps | Omit requirements.txt when the script uses only the standard library |
| Wrapping a stdlib-only script in Bash | Adds setup indirection without solving a real dependency problem | Document direct python3 <script>.py execution and skip the wrapper |
Shipping a loose .py file with undocumented setup steps | Users must guess how to run the tool safely | Generate a self-contained folder and add run.sh plus requirements.txt only when external packages are needed |
Treating a multi-entrypoint toolkit as app code just because it has lib/ and tests | Pushes script tooling into the wrong guidance lane | Keep it in internal-script-python when the primary contract is still direct execution |
Copying the same .venv bootstrap and dependency install code into every wrapper | Maintenance drift and inconsistent operator behavior | Use one shared run.sh and let thin wrappers delegate to it |
| Assuming the tool will always run from the repository root | Breaks when operators call it from subdirectories or nested paths | Resolve the repo root from an explicit --root or path argument when needed |
| Adding JSON output without a real machine consumer | Increases surface area and maintenance cost | Keep text output first and add --format json only when automation needs it |
| Defaulting to stdlib without comparing mature libraries | Leaves avoidable boilerplate, edge cases, and custom parsing logic in the script | Write the dependency decision note first and choose the option that makes the final code simpler |
| Rejecting a useful dependency just to keep dependency count low | Optimizes the wrong thing and increases custom code | Optimize for simpler final code and justified value, not dependency minimization |
| Forcing async or framework abstractions into a simple tool | Raises complexity without improving the script | Keep the script synchronous and direct unless concurrency is essential |
python -m py_compile <script_name>.py (syntax check)bash -n run.sh (launcher syntax check, only when run.sh exists)pytest tests/ (run tests)python -m compileall <changed_paths> or the repository's canonical shared runner when the tool already lives inside a maintained toolkit