skills/setting-up-python-projects/SKILL.md
ALWAYS LOAD THIS SKILL WHEN CREATING A NEW PYTHON PROJECT OR SETTING UP PROJECT STRUCTURE. Do not scaffold or bootstrap Python projects directly — use this skill first. Bootstrap new Python projects: directory structure, pyproject.toml, pre-commit, uv sync.
npx skillsauth add quick-brown-foxxx/coding_rules_python setting-up-python-projectsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
New projects start with the full safety net configured. Templates are in the repo: https://github.com/quick-brown-foxxx/coding_rules_python/tree/master/templates`.
Make sure to read repo's readme.
project/
├── src/appname/
│ ├── __init__.py # __version__ = "0.1.0"
│ ├── __main__.py # Entry point
│ ├── constants.py # Shared constants
│ ├── core/ # Business logic
│ │ ├── models.py # Data types (dataclasses)
│ │ ├── manager.py # Business operations
│ │ └── exceptions.py # Custom exception hierarchy
│ ├── cli/ # CLI interface
│ │ ├── commands.py # Command implementations
│ │ ├── parser.py # Argument parsing
│ │ └── output.py # Formatted output helpers
│ ├── ui/ # Qt GUI (if applicable)
│ │ ├── main_window.py
│ │ ├── dialogs/
│ │ └── widgets/
│ ├── utils/ # Stateless utilities
│ │ ├── paths.py
│ │ └── logging.py
│ ├── wrappers/ # Third-party lib wrappers
│ │ └── some_wrapper.py
│ └── stubs/ # Type stubs for untyped libs
├── tests/
│ ├── unit/
│ ├── integration/
│ ├── fixtures/
│ └── conftest.py
├── scripts/ # Dev utilities
│ ├── bootstrap.py # Setup script
│ └── check_type_ignore.py
├── docs/
│ ├── coding_rules.md # Copy from rules/coding_rules.md
│ └── PHILOSOPHY.md # Copy from PHILOSOPHY.md
├── shared/ # Cross-cutting (copy from coding_rules_python/reusable/)
│ ├── logging/ # Logging + colored output (if needed)
│ └── shortcuts/ # Keyboard shortcuts (if PySide6 app)
├── AGENTS.md # Copy from templates/AGENTS.md, customize
├── CLAUDE.md # Symlink → AGENTS.md
├── pyproject.toml # Copy from templates/pyproject.toml, customize
├── .pre-commit-config.yaml # Copy from templates/pre-commit-config.yaml
├── .gitignore # Copy from templates/gitignore
└── .vscode/
├── settings.json # Copy from templates/vscode_settings.json
└── extensions.json # Copy from templates/vscode_extensions.json
Create directory structure:
mkdir -p src/APPNAME tests/unit tests/integration tests/fixtures scripts docs .vscode
Copy template and reference files:
templates/pyproject.toml → pyproject.toml (update [project] section)templates/AGENTS.md → AGENTS.md (fill TODO sections)templates/pre-commit-config.yaml → .pre-commit-config.yamltemplates/gitignore → .gitignoretemplates/vscode_settings.json → .vscode/settings.jsontemplates/vscode_extensions.json → .vscode/extensions.jsonrules/coding_rules.md → docs/coding_rules.mdPHILOSOPHY.md → docs/PHILOSOPHY.mdln -s AGENTS.md CLAUDE.mdCopy reusable code (if needed):
coding_rules_python/reusable/ copy modules you need into shared/logging/ — colored logging, file rotating logs, CLI output (see setting-up-logging skill)shortcuts/ — keyboard shortcuts for PySide6 apps (see setting-up-shortcuts skill)coding_rules_python/reusable_tests/ into your tests/ (e.g., test_shortcuts_base.py, test_shortcuts_manager.py)reusable. → shared. or your package path, reusable_tests. → your test package)Create entry points:
# src/APPNAME/__init__.py
__version__ = "0.1.0"
# src/APPNAME/__main__.py
from __future__ import annotations
import argparse
import sys
from pathlib import Path
CLI_WORDS = {"config"} # Top-level CLI entry words for this app
def parse_gui_request(argv: list[str]) -> tuple[Path | None, bool] | None:
parser = argparse.ArgumentParser(add_help=False, allow_abbrev=False)
parser.add_argument("path", nargs="?")
parser.add_argument("--debug", action="store_true")
try:
ns, unknown = parser.parse_known_args(argv)
except SystemExit:
return None
if unknown:
return None
path = Path(ns.path).expanduser() if isinstance(ns.path, str) else None
return path, bool(ns.debug)
def main() -> int:
argv = sys.argv[1:]
if argv and (argv[0] in {"-h", "--help"} or argv[0] in CLI_WORDS):
from APPNAME.bootstrap import create_services
from APPNAME.cli import build_cli_app
services = create_services(debug=False)
app = build_cli_app(services)
app(args=argv, prog_name="APPNAME", standalone_mode=False)
return 0
gui_request = parse_gui_request(argv)
if gui_request is not None:
from APPNAME.gui import run_gui
path, debug = gui_request
return run_gui(path=path, debug=debug)
from APPNAME.bootstrap import create_services
from APPNAME.cli import build_cli_app
services = create_services(debug=False)
app = build_cli_app(services)
app(args=argv, prog_name="APPNAME", standalone_mode=False)
return 0
if __name__ == "__main__":
sys.exit(main())
__main__.py is the router only. Keep Typer assembly in APPNAME.cli, keep GUI startup in APPNAME.gui, and avoid len(sys.argv) > 1 heuristics. The tiny pre-parse only answers “is this a GUI-shaped invocation?” so APPNAME, APPNAME file.txt, and APPNAME --debug can reach the GUI, while APPNAME -h and APPNAME config ... stay in Typer.
Create initial test:
# tests/test_main.py
from APPNAME.__main__ import main
def test_main_runs(capsys: pytest.CaptureFixture[str]) -> None:
assert main() == 0
Initialize environment:
git init
uv sync --all-extras --group dev
uv run pre-commit install
uv run poe lint_full
uv run poe test
Verify everything works:
uv run poe app runs the applicationuv run poe lint_full passes with 0 errorsuv run poe test passesDesign every app to be interruptible without corruption, hanging, or ugly tracebacks. The shutdown strategy depends on what the app does:
App type → Strategy
─────────────────────────────────────────────────────────────
Simple script/CLI → catch KeyboardInterrupt, exit 130
CLI wrapping a quick subtask → kill process group immediately
CLI wrapping complex tool (vagrant…) → SIGTERM → wait → SIGKILL
Qt/async app → see building-qt-apps skill
# __main__.py
def main() -> int:
try:
return run()
except KeyboardInterrupt:
return 130 # 128 + SIGINT(2), Unix convention
Always pass start_new_session=True — creates a process group so you can kill the entire tree, not just the parent.
Quick subtask (immediate kill):
import os, signal, subprocess
process = subprocess.Popen(cmd, start_new_session=True)
try:
process.wait()
except KeyboardInterrupt:
os.killpg(process.pid, signal.SIGKILL)
Complex tool wrapper (escalation):
process = subprocess.Popen(cmd, start_new_session=True)
try:
process.wait()
except KeyboardInterrupt:
os.killpg(process.pid, signal.SIGTERM)
try:
process.wait(timeout=5.0)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
Async subprocess (complex apps using asyncio):
process = await asyncio.create_subprocess_exec(*cmd, start_new_session=True)
try:
await process.wait()
except asyncio.CancelledError:
process.terminate()
try:
await asyncio.wait_for(process.wait(), timeout=5.0)
except TimeoutError:
process.kill()
raise
# scripts/bootstrap.py
"""Set up development environment."""
import subprocess
def main() -> None:
subprocess.run(["uv", "sync", "--all-extras", "--group", "dev"], check=True)
subprocess.run(["uv", "run", "pre-commit", "install"], check=True)
print("Development environment ready.")
if __name__ == "__main__":
main()
After scaffolding, adapt everything to the specific project. The templates are a starting point, not a straitjacket. docs/PHILOSOPHY.md is the only ruling constant — everything else bends to fit the project's tech stack, domain, and constraints.
| Area | How to adapt |
|------|--------------|
| Directory layout | Add/remove/rename directories to match the domain. Not every project needs cli/, ui/, wrappers/, shared/. A data pipeline might need pipelines/, schemas/, extractors/. A web service might need routes/, middleware/, repositories/. |
| Dependencies | Add domain-specific libraries. Remove unused template defaults. Research current best-in-class libraries for the domain (e.g. SQLAlchemy vs raw asyncpg, Pydantic vs attrs). |
| pyproject.toml | Adjust ruff rules, pytest markers, basedpyright overrides for the domain. Some domains need relaxed rules (e.g. data science may need broader type: ignore for numpy interop). |
| AGENTS.md | Fill TODO sections with project-specific architecture, key decisions, domain vocabulary, and workflows. This is the agent's primary orientation document — make it specific. Skills section: remove skills the project won't use (e.g. building-multi-ui-apps for a pure CLI), add domain-specific skills (e.g. building-qt-apps, setting-up-shortcuts). |
| coding_rules.md | Extend or override rules for the domain. Add domain-specific conventions (e.g. database migration rules, API versioning policy, data validation requirements). |
| Test structure | Adjust to match what matters. A CLI tool needs heavy e2e tests. A library needs heavy unit tests. A web service needs API integration tests. |
| CI/CD | Add domain-appropriate checks (e.g. migration consistency, API schema validation, container builds). |
When the project wraps third-party libraries (for typing, platform abstraction, or swappability), enforce wrapper usage via ruff's flake8-tidy-imports.banned-api in pyproject.toml:
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"soundcard".msg = "Use src/wrappers/audio_backend.py instead"
"faster_whisper".msg = "Use src/wrappers/transcriber.py instead"
Wrap when a library is poorly typed (need typed facade), platform-specific (need abstraction layer), or swappable (need stable internal API). The template pyproject.toml has commented examples — uncomment and customize per project.
Inside the wrapper files themselves, suppress the ban with a per-file ruff ignore: "src/wrappers/*".msg = "" in the banned-api config, or use # noqa: TID251 on individual import lines.
When setting up a project in an unfamiliar domain or with unfamiliar libraries:
development
ALWAYS LOAD THIS SKILL WHEN A NEW FEATURE, NON-TRIVIAL FIX, REFACTOR, OR PYTHON STRUCTURE CHANGE REQUIRES AN ARCHITECTURE DECISION ABOUT LAYERS, WRAPPERS, COMPOSITION ROOTS, FRAMEWORK CHOICES, REUSABLE CORES, OR WHERE CODE SHOULD LIVE. Do not make Python architecture decisions blindly — use this skill first. Python architecture guide + skill router for boundary placement, reusable core design, composition vs inheritance, framework vs custom choices, backend/service layering, and follow-up docs/skills.
tools
ALWAYS LOAD THIS SKILL WHEN CREATING ANY STANDALONE PYTHON SCRIPT OR SINGLE-FILE AUTOMATION. Do not create Python scripts directly — use this skill first. Single-file Python scripts with PEP 723 inline metadata, uv run, and typer CLI.
development
ALWAYS LOAD THIS SKILL WHEN WRITING OR EDITING PYTHON CODE. Do not write or modify Python files directly — use this skill first. Core Python standards: basedpyright strict typing, Result-based error handling, async patterns, security, code style.
development
ALWAYS LOAD THIS SKILL WHEN WRITING TESTS, ADDING FIXTURES, OR SETTING UP PYTEST. Do not write Python tests directly — use this skill first. Python testing with pytest: philosophy, fixtures, mock servers, containerized testing.