diff --git a/docs/standards/python_coding_standards.md b/docs/standards/python_coding_standards.md index cf3185b..62c7fb2 100644 --- a/docs/standards/python_coding_standards.md +++ b/docs/standards/python_coding_standards.md @@ -4,10 +4,68 @@ This document defines the internal development standards for building backend se **Note:** Python should **ONLY** be used for creating backend services related to AI or data science. For frontend services, use Node.js using the following [Node.js](https://defra.github.io/software-development-standards/standards/node_standards/) and [Javascript](https://defra.github.io/software-development-standards/standards/javascript_standards/) Defra standards. -## Environments -These standards advises the use of uv to manage different versions of Python you have installed. +## Package Management -To create virtual environments call `uv venv -p python3.13` from your project root directory. This will create a virtual environment with that specific python version in a folder called `.venv`. This folder should be excluded in your `.gitignore` file. For more information see [Python virtual environment primer](https://realpython.com/python-virtual-environments-a-primer/) +These standards mandate using [uv](https://docs.astral.sh/uv/) for all Python project management, including dependency resolution, version pinning, and virtual environment handling. All new Python projects must be created using `uv init` and follow the standard project structure it generates. + +### Virtual Environments + +`uv` manages virtual environments automatically—they are created transparently when you run `uv sync` or `uv run`. No manual environment setup is required. + +### pyproject.toml + +All projects must use `pyproject.toml` for declaring dependencies and project metadata. This file is automatically generated and managed by `uv init`. + +### Dependency Management + +- Pin dependencies to exact versions using `==` specifiers to prevent unexpected version drift: + +```toml +dependencies = [ + "fastapi==0.104.1", + "requests==2.31.0", +] +``` + +- Commit `uv.lock` to version control for reproducible builds. Always use `uv sync --locked` in CI/automated builds to ensure the lock file matches `pyproject.toml` (equivalent to `npm ci`). + +- Separate dependencies and dev dependencies using `[project.optional-dependencies]` or `[dependency-groups]`. + +- Use an automated dependency checker such as Dependabot or `uv pip audit` to monitor for security vulnerabilities and keep dependencies up to date. + +- Before adding third-party packages, vet them according to the [package vetting guide](../guides/choosing_packages.md). + +#### pyproject.toml security settings + +Create the following configuration in `pyproject.toml` to implement supply-chain security controls: + +```toml +[tool.uv] +exclude-newer = "1 week" +``` + +| Setting | Purpose | +|---|---| +| `exclude-newer = "1 week"` | Refuses to resolve packages published fewer than 7 days ago. Provides a window to detect package takeover or typosquatting attacks before they reach your codebase. | +| `exclude-newer-package` | Per-package override of the quarantine window. Set to the RFC 3339 timestamp of the specific release needed — use this when a critical vulnerability fix must be resolved before the quarantine expires. | + +Example with a critical patch override: + +```toml +[tool.uv] +exclude-newer = "1 week" +exclude-newer-package = { requests = "2026-05-10T00:00:00Z" } +``` + +When using `exclude-newer-package` to bypass the quarantine for a critical security patch, you must closely examine the package for signs of supply chain compromise before adding it to your project: + +- Verify the package on the official registry (PyPI) and check the release notes for legitimacy +- Review the package source repository—look for unusual activity, unexpected commits, or maintainer changes +- Check package download statistics for anomalies +- If possible, compare the package hash against official sources +- Review package dependencies for suspicious additions + +Remove the override once the package ages past the global `exclude-newer` window. ## Linting @@ -21,27 +79,27 @@ These standards advises the use of the [Ruff](https://docs.astral.sh/ruff/) comm - Use standard [PEP 484](https://peps.python.org/pep-0484/) typing syntax. - Function definitions should format parameters and return types as follows: - ```python - def get_item(item_id: int, detail: bool = False) -> dict[str, str]: - ... - ``` +```python +def get_item(item_id: int, detail: bool = False) -> dict[str, str]: + ... +``` - Annotate variables where the type is not immediately clear: - ```python - items: list[str] = [] - ``` +```python +items: list[str] = [] +``` - Optional types should be annotated with | None or Optional from typing: - ```python - def get_user(user_id: int | None = None) -> dict[str, str] | None: - ... - ``` +```python +def get_user(user_id: int | None = None) -> dict[str, str] | None: + ... +``` - There should be no spaces before the colon and exactly one space after. - ```python - def get_mapping() -> dict[str, int]: - return {'a': 1, 'b': 2} - ``` +```python +def get_mapping() -> dict[str, int]: + return {'a': 1, 'b': 2} +``` ## Naming Conventions @@ -49,22 +107,24 @@ These standards advises the use of the [Ruff](https://docs.astral.sh/ruff/) comm - Classes: Use PascalCase. - Constants: Use UPPER_CASE. - Example: - ```python - MAX_RETRIES = 3 +Example: +```python +MAX_RETRIES = 3 + +def calculate_total(items: list[int]) -> int: + return sum(items) - def calculate_total(items: list[int]) -> int: - return sum(items) +class DataProcessor: + pass +``` - class DataProcessor: - pass - Private members should start with a single underscore (_). - Exception class names should end in Error. - ```python - class ValidationError(Exception): - pass - ``` +```python +class ValidationError(Exception): + pass +``` ## Import Style @@ -78,80 +138,76 @@ These standards advises the use of the [Ruff](https://docs.astral.sh/ruff/) comm - Each group must be separated by one blank line. - Example: - ```python - import os - import sys +Example: +```python +import os +import sys - from fastapi import FastAPI - import requests +from fastapi import FastAPI +import requests - from app.models import User - from app.services import user_service - ``` +from app.models import User +from app.services import user_service +``` - Imports should be one per line. - - Absolute imports should be used. - - Wildcard imports (`from module import *`) should not be used. ## Error Handling - Specific exceptions should be caught rather than using a bare `except:`. - - When raising exceptions, include an informative error message. - - Exceptions that represent a domain-specific error should subclass `Exception` and be suffixed with `Error`. - Example: - ```python - from fastapi import HTTPException +Example: +```python +from fastapi import HTTPException - def fetch_data(url: str) -> str: - try: - response = requests.get(url) - response.raise_for_status() - except requests.RequestException as exc: - raise HTTPException(status_code=500, detail=str(exc)) - return response.text - ``` +def fetch_data(url: str) -> str: + try: + response = requests.get(url) + response.raise_for_status() + except requests.RequestException as exc: + raise HTTPException(status_code=500, detail=str(exc)) + return response.text +``` ## File and Module Structure - Modules and packages must use short, all-lowercase names. Underscores can be used if necessary. - - Each module must have a single, clear responsibility. - Example project structure: - ``` - - app/ - ├── main.py - ├── api/ - │ ├── users.py - │ └── items.py - ├── models/ - │ └── user.py - ├── services/ - │ └── user_service.py - ├── utils/ - │ └── helpers.py - ├── config.py - - ``` +Example project structure: +```bash +app/ +├── main.py +├── api/ +│ ├── users.py +│ └── items.py +├── models/ +│ └── user.py +├── services/ +│ └── user_service.py +├── utils/ +│ └── helpers.py +├── config.py +``` ## Constants and Configuration - Constants must be defined using UPPER_CASE. - - Configuration values should be loaded from environment variables where possible. - Example: - ```python - import os +Example: +```python +import os + +MAX_RETRIES = 5 +DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db") +``` + +- Hard-coded configuration values should be avoided. - MAX_RETRIES = 5 - DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db") - ``` +## Significant changes -- Hard-coded configuration values should be avoided. \ No newline at end of file +Package Management section and `exclude-newer` security settings added 13 May 2026. \ No newline at end of file