Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 135 additions & 79 deletions docs/standards/python_coding_standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -21,50 +79,52 @@ 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

- Variables and functions: Use snake_case.
- 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

Expand All @@ -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.
Package Management section and `exclude-newer` security settings added 13 May 2026.