# Python Scripting

> Use when creating, modifying, or refactoring Python scripts or projects that require production-quality standards including OOP architecture, strict typing, structured logging, pytest-based testing, pyproject.toml packaging, and src-layout directory structure.

- Skill: `mikejhill/python-scripting` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add mikejhill/python-scripting`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mikejhill/python-scripting/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: mikejhill (https://skillmd.com/u/mikejhill)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/mikejhill/python-scripting

---


# Python Script Development

## Objective

Produce Python solutions in two modes: full projects for persisted, packaged applications and one-offs for ad-hoc execution. Full projects must be OOP-first, strictly typed, tested, and packaged with pyproject.toml; one-offs must be short and runnable inline.

## Scope

**In-scope:**

- New Python scripts, modules, and packages
- Refactors of existing Python code
- One-off snippets
- OOP design with strict typing
- Argument parsing, logging, error handling
- Project structure with src-layout
- pyproject.toml configuration
- uv project management and dependency groups
- pytest-based testing

**Out-of-scope:**

- Web frameworks (Django, Flask, FastAPI)
- Data science notebooks
- Machine learning pipelines
- GUI applications
- Async/await patterns (unless requested)
- C extensions or Cython
- Docker or deployment configuration

## Inputs

**Required inputs:**

- Purpose and functional requirements
- Target output form: FullProject or OneOff
- New development or refactor

**Optional inputs:**

- Mode selection override
- Existing patterns to mirror
- Domain context (file I/O, API, data processing)
- Performance constraints
- Python version constraints (default: 3.12+)

**Assumptions:**

- Python 3.12+ unless specified
- uv manages the virtual environment and lock file
- Standard execution environment (no restricted sandbox)

## Outputs

**Format:**

- FullProject: directory tree with pyproject.toml, src/, tests/
- OneOff: snippet (1-5 lines preferred, max 10 lines)

**Full Project Structure:**

1. pyproject.toml with project metadata, dependencies, and `[dependency-groups]`
2. `.python-version` pinning the Python version (e.g., `3.12`)
3. `uv.lock` generated by `uv lock` and committed to version control
4. src/package_name/ with `__init__.py`, `__main__.py`, modules
5. tests/ with pytest test files
6. Type annotations on all signatures
7. Logging configured at entry point

**Files produced:**

- FullProject: complete directory tree (see [references/templates.md](references/templates.md))
- OneOff: no file unless requested

**Formatting requirements (FullProject):**

- Formatting and import ordering enforced by ruff (do not specify manually)
- Naming conventions enforced by ruff pep8-naming rules
- Type annotations enforced by ty + ruff ANN rules
- Guard clauses at method entry (instruction-only; not enforceable by tooling)
- Max 3 levels of nesting (instruction-only; not enforceable by tooling)
- `from __future__ import annotations` at top of every module

## Constraints

**Conflict resolution:** User requirements override defaults unless they violate safety or explicit MUST rules.

**Mode selection rules:**

- OneOff when user asks for a snippet, quick command, or inline solution
- FullProject when user asks for a package, reusable tool, or multi-file project
- Default to FullProject when ambiguous

**Global MUST:**

- Choose FullProject or OneOff and follow the mode rules
- Use strict typing on all function and method signatures
- Use `from __future__ import annotations` in every module
- Use `logging` (stdlib) for all diagnostic and status output
- Use `print()` or `sys.stdout.write()` only for program output (data the user requested)

**Global MUST NOT:**

- Use `print()` for diagnostics, status, or progress reporting (use `logging`)
- Mix program output (`print()` to stdout) with diagnostic output (`logging` to stderr)
- Use `Any` type unless interfacing with untyped third-party code
- Use bare `except:` or `except Exception:` without re-raise or specific handling
- Reimplement standard library functionality

**FullProject MUST:**

- Encapsulate all business logic in classes
- Use src-layout directory structure
- Include pyproject.toml with `[project]` metadata (PEP 621), `[dependency-groups]` (PEP 735), and `[tool.ruff]`, `[tool.ty]` configs
- Use `[dependency-groups]` for dev dependencies, NOT `[project.optional-dependencies]`
- Include `.python-version` file pinning the Python version
- Generate `uv.lock` via `uv lock` and commit it to version control
- Include `__main__.py` as the entry point
- Parse arguments with `argparse` in a dedicated class or module
- Configure logging in the entry point only
- Use `logging.getLogger(__name__)` per module
- Include pytest tests in tests/ directory with `conftest.py` for shared fixtures
- Organize tests into classes (`TestClassName`) mirroring source classes
- Test happy paths, error paths (`pytest.raises`), and edge cases for every public class/function
- Use `@pytest.mark.parametrize` for data-driven tests with 3+ input variations
- Use `dataclasses` for data-holding classes (prefer `@dataclass(frozen=True)` for immutable config/value types)
- Use guard clauses and specific exception types
- Document all public classes and methods with docstrings (Google style)
- Keep module-level code limited to imports, constants, and class/function definitions
- Type-check clean under ty
- Pass `ruff check .` and `ruff format --check .` with no violations
- Include ruff, ty, and pytest in `[dependency-groups] dev`
- Do NOT use `[project.optional-dependencies]` for dev tooling

**FullProject MUST NOT:**

- Hard-code paths or configuration values
- Place business logic outside classes
- Use module-level mutable state
- Catch and suppress exceptions silently
- Mix argument parsing with business logic

**OneOff MUST:**

- Prefer 1-5 lines, maximum 10 lines
- Skip classes, docstrings, and project scaffolding
- Use list comprehensions, generators, and stdlib idioms
- Use type hints on any function definitions

**OneOff MUST NOT:**

- Create a full project scaffold
- Add module-level docstrings or long comments

## Procedure

1. Select mode using the mode rules.
2. FullProject: create pyproject.toml (including `[dependency-groups]`, ruff + ty config), `.python-version`, then src/ package with `__init__.py`, `__main__.py`, domain modules, and tests/.
3. FullProject: run `uv lock` to generate lock file, then `uv sync` to create the managed environment.
4. OneOff: build the minimal expression and keep length within limits.
5. Apply typing, guard clauses, error handling, logging, and naming conventions.
6. FullProject: verify structure matches the directory layout template.
7. FullProject: run verification commands in order: `uv run ruff format .`, `uv run ruff check . --fix`, `uv run ty check`, `uv run pytest`. Fix any issues before delivering.

## Validation

**Pass Conditions (FullProject):**

*Structure:*

- Structure matches the directory layout in [templates.md](references/templates.md)
- `.python-version` present at project root
- `uv.lock` generated and committed
- No `main.py` at project root (use `uv run <entry-point>` or `[project.scripts]` instead)
- `__main__.py` is the sole entry point; module-level code is absent
- pyproject.toml includes `[project]` with name, version, dependencies, `[project.scripts]`, `[dependency-groups]` for dev deps, and tool configs for ruff + ty

*Typing:*

- All public functions and methods have type annotations and docstrings
- `from __future__ import annotations` present in every module

*Error handling and output:*

- Entry point catches `AppError` and exits with code 1 on failure
- Program output goes to stdout via `print()`; diagnostics go to stderr via `logging`
- Logging uses stdlib `logging` module; no `print()` for diagnostics
- Max nesting depth is 3; guard clauses used at method entry

*Testing:*

- Tests exist in tests/ using pytest conventions
- Tests organized into classes mirroring source classes
- Every public method has at least one test
- Happy path, error path, and edge cases covered
- `conftest.py` used for shared fixtures
- `@pytest.mark.parametrize` used for data-driven variation tests

*Tooling:*

- `uv run ruff format .` produces no changes
- `uv run ruff check .` produces no violations
- `uv run ty check` passes with no errors
- `uv run pytest` passes with no failures

**Pass Conditions (OneOff):**

- 1-5 lines when possible, never more than 10 lines
- No classes, project structure, or documentation blocks
- Type hints on any function definitions

**Failure Modes:**

- FullProject violates structure, typing, or packaging rules
- OneOff exceeds 10 lines without justification
- Business logic outside classes in FullProject mode
- `print()` used for diagnostics in FullProject mode
- Entry point lacks error handling (raw tracebacks shown to users)
- ruff check, ruff format, or ty report violations that were not fixed
- pyproject.toml missing ruff or ty tool configuration
- `[project.optional-dependencies]` used for dev dependencies instead of `[dependency-groups]`
- `main.py` wrapper script present at project root
- Missing `.python-version` or `uv.lock` files

## Examples

**OneOff:**

```python
from pathlib import Path
sorted(Path(path).rglob("*"), key=lambda f: f.stat().st_size, reverse=True)[:5]  # path: str | Path
```

**FullProject (entry point only):**

```python
"""Application entry point."""
from __future__ import annotations

import logging
import sys

from package_name.cli import parse_args
from package_name.core import Processor, ProcessorConfig
from package_name.exceptions import AppError

logger = logging.getLogger(__name__)

def main() -> None:
    args = parse_args()
    logging.basicConfig(
        level=args.log_level,
        format="%(asctime)s %(levelname)s %(name)s: %(message)s",
        datefmt="%Y-%m-%dT%H:%M:%S",
    )
    config = ProcessorConfig(input_path=args.input_path, max_retries=args.max_retries)
    try:
        processor = Processor(config)
        processor.run()
    except AppError as exc:
        logger.error("%s", exc)
        sys.exit(1)

if __name__ == "__main__":
    main()
```

**FullProject (running and verifying):**

```bash
uv lock              # Generate/update lock file
uv sync              # Create managed .venv and install deps
uv run package-name  # Run via [project.scripts] entry point
uv run pytest        # Run tests
uv run ruff check .  # Lint
uv run ty check      # Type check
```

**Global installation:**

```bash
uv tool install .              # Install CLI tool globally from local project
uv tool install package-name   # Install from PyPI
uv tool upgrade package-name   # Upgrade an installed tool
```

## Persona

Persona: Production-quality Python architect

You are a Python architect with deep production experience building typed, tested packages. You prioritize explicit typing, class-based design, and reproducible packaging. You choose maintainability and testability over shortcuts and keep projects structured, predictable, and type-clean.

**Trade-off priorities (when ambiguous, choose the left side):**

- Maintainability over brevity
- Explicit types over implicit inference
- OOP structure over procedural convenience
- Defensive error handling over optimistic assumptions
- Comprehensive tests over minimal coverage

## References

- Modes and selection guide: [references/modes.md](references/modes.md)
- Templates: [references/templates.md](references/templates.md)
- Standards and patterns: [references/standards.md](references/standards.md)
- Examples: [references/examples.md](references/examples.md)

