Python Script Instructions

June 26, 2026 ยท View on GitHub

Conventions for Python 3.11+ scripts used in automation, tooling, and CLI applications.

Entry Points and Exit Codes

import sys

EXIT_SUCCESS = 0  # Successful execution
EXIT_FAILURE = 1  # General failure
EXIT_ERROR = 2    # Arguments or configuration error


def main() -> int:
    """Main entry point for the script."""
    return EXIT_SUCCESS


if __name__ == "__main__":
    sys.exit(main())

Standard exit codes: 0 success, 1 failure, 2 configuration error, 130 user interrupt (SIGINT).

CLI Argument Parsing

argparse

Extract parser creation into a separate function for testability.

import argparse
from pathlib import Path


def create_parser() -> argparse.ArgumentParser:
    """Create and configure argument parser."""
    parser = argparse.ArgumentParser(description="Process files")
    parser.add_argument("-v", "--verbose", action="store_true")
    parser.add_argument("-o", "--output", type=Path, default=Path("output.txt"))
    parser.add_argument("input_file", type=Path)
    return parser

Use type=Path for file arguments and action="store_true" for boolean flags.

click

For complex CLIs with subcommands or interactive prompts, use the click framework.

import click


@click.command()
@click.option("-v", "--verbose", is_flag=True)
@click.argument("input_file", type=click.Path(exists=True))
@click.pass_context
def main(ctx: click.Context, verbose: bool, input_file: str) -> None:
    """Process input files."""
    ctx.exit(0)  # Explicit exit code

Use @click.group() for subcommands, ctx.exit(code) for exit codes, and ctx.fail(message) for errors.

Logging Configuration

import logging

logger = logging.getLogger(__name__)


def configure_logging(verbose: bool = False) -> None:
    """Configure logging based on verbosity level."""
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(level=level, format="%(levelname)s: %(message)s")

Create module-level logger, configure early in main. For file logging, add FileHandler to the root logger.

Path Handling

Use pathlib.Path exclusively; avoid os.path.

from pathlib import Path


def process_file(path: Path) -> None:
    """Read, process, and write file content."""
    content = path.read_text(encoding="utf-8")
    processed = transform_content(content)
    output_path = path.with_suffix(".out")
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(processed, encoding="utf-8")

Common patterns: cwd(), resolve(), exists(), is_dir(), is_file(), iterdir(), glob(), rglob(), read_text(), write_text(), mkdir(parents=True, exist_ok=True), parent, name, stem, suffix.

Subprocess Execution

Use subprocess.run() with error handling.

import subprocess
import os
from pathlib import Path


def run_command(cmd: list[str], cwd: Path | None = None, extra_env: dict[str, str] | None = None) -> str:
    """Run command and return stdout, raising on failure."""
    env = os.environ.copy()
    if extra_env:
        env.update(extra_env)
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, check=True, cwd=cwd, env=env)
        return result.stdout
    except subprocess.CalledProcessError as e:
        logger.error("Command failed: %s\nstderr: %s", e.returncode, e.stderr)
        raise
    except FileNotFoundError:
        logger.error("Command not found: %s", cmd[0])
        raise

Use capture_output=True and text=True for string output. Use check=True to raise on non-zero exit.

Type Hints

Use Python 3.11+ syntax with built-in generics.

from pathlib import Path
from typing import Literal, Self


def process_items(items: list[str]) -> dict[str, int]:  # Built-in generics
    return {item: len(item) for item in items}


def read_file(path: str | Path) -> str:  # Union with pipe
    return Path(path).read_text(encoding="utf-8")


def find_config(name: str) -> Path | None:  # Optional with pipe
    config = Path(name)
    return config if config.exists() else None


def set_level(level: Literal["debug", "info", "warning"]) -> None:  # Constrained values
    pass


class Builder:
    def add(self, item: str) -> Self:  # Fluent interface
        self.items.append(item)
        return self

Use list[str] not typing.List[str], str | None not Optional[str], Literal for constrained values, Self for chained methods.

Error Handling

Handle interrupts and pipe errors at the top level.

import sys


def main() -> int:
    """Main entry point with error handling."""
    try:
        return run()
    except KeyboardInterrupt:
        print("\nInterrupted by user", file=sys.stderr)
        return 130
    except BrokenPipeError:
        sys.stderr.close()
        return 1
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        return 1

Custom exceptions can carry exit codes:

class ScriptError(Exception):
    def __init__(self, message: str, exit_code: int = 1) -> None:
        super().__init__(message)
        self.exit_code = exit_code

Documentation

Use Google-style docstrings with Args, Returns, Raises, and Example sections.

def process_data(data: list[str], *, normalize: bool = False) -> dict[str, int]:
    """Process input data and return statistics.

    Args:
        data: List of strings to process.
        normalize: If True, normalize values before processing.

    Returns:
        Dictionary mapping processed items to their counts.

    Raises:
        ValueError: If data is empty.

    Example:
        >>> process_data(["a", "b", "a"])
        {'a': 2, 'b': 1}
    """

Include module docstrings with description, usage, and examples.

Script Organization

Organize scripts in this order:

  1. Shebang: #!/usr/bin/env python3
  2. Copyright header: # Copyright (c) 2026 Microsoft Corporation. All rights reserved.
  3. SPDX license identifier: # SPDX-License-Identifier: MIT
  4. PEP 723 inline script metadata (if applicable)
  5. Future imports: from __future__ import annotations
  6. Imports: standard library, third-party, local (separated by blank lines)
  7. Constants and exit codes
  8. Module-level logger
  9. Helper functions
  10. Parser creation function
  11. Logging configuration function
  12. Run logic function
  13. Main entry point
  14. Module guard: if __name__ == "__main__": sys.exit(main())

Inline Script Metadata

PEP 723 inline metadata enables automatic dependency installation with uv.

#!/usr/bin/env python3
# Copyright (c) 2026 Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: MIT
# /// script
# requires-python = ">=3.11"
# dependencies = [
#     "click>=8.0",
#     "rich>=13.0",
# ]
# ///

Place after copyright and SPDX headers, before module docstring. Run with uv run script.py.