|
| 1 | +# Python Standards |
| 2 | + |
| 3 | +This document defines the internal development standards for building backend services in Python ([FastAPI](https://fastapi.tiangolo.com/tutorial/)) within Defra. It adheres strictly to the [UK Government Digital Service (GDS) Python style guide](https://gds-way.digital.cabinet-office.gov.uk/manuals/programming-languages/python/python.html) and [PEP 8](https://peps.python.org/pep-0008/), ensuring code is clear, consistent, and maintainable across all AI services (using tools like [LangChain](https://python.langchain.com/docs/introduction/), [LangGraph](https://langchain-ai.github.io/langgraph/tutorials/introduction/), etc.). All developers must follow these conventions when writing Python code for backend services. |
| 4 | + |
| 5 | +**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. |
| 6 | + |
| 7 | +## Environments |
| 8 | +These standards advises the use of uv to manage different versions of Python you have installed. |
| 9 | + |
| 10 | +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/) |
| 11 | + |
| 12 | +## Linting |
| 13 | + |
| 14 | +### Ruff |
| 15 | +These standards advises the use of the [Ruff](https://docs.astral.sh/ruff/) command line checker as an all in one formatter, linter, codestyle and complexity checker. |
| 16 | + |
| 17 | + |
| 18 | +## Function Annotations and Typing |
| 19 | + |
| 20 | +- All public functions must include type annotations for parameters and return types. |
| 21 | +- Use standard [PEP 484](https://peps.python.org/pep-0484/) typing syntax. |
| 22 | +- Function definitions should format parameters and return types as follows: |
| 23 | + |
| 24 | + ```python |
| 25 | + def get_item(item_id: int, detail: bool = False) -> dict[str, str]: |
| 26 | + ... |
| 27 | + ``` |
| 28 | +- Annotate variables where the type is not immediately clear: |
| 29 | + |
| 30 | + ```python |
| 31 | + items: list[str] = [] |
| 32 | + ``` |
| 33 | +- Optional types should be annotated with | None or Optional from typing: |
| 34 | + ```python |
| 35 | + def get_user(user_id: int | None = None) -> dict[str, str] | None: |
| 36 | + ... |
| 37 | + ``` |
| 38 | + |
| 39 | +- There should be no spaces before the colon and exactly one space after. |
| 40 | + |
| 41 | + ```python |
| 42 | + def get_mapping() -> dict[str, int]: |
| 43 | + return {'a': 1, 'b': 2} |
| 44 | + ``` |
| 45 | + |
| 46 | +## Naming Conventions |
| 47 | + |
| 48 | +- Variables and functions: Use snake_case. |
| 49 | +- Classes: Use PascalCase. |
| 50 | +- Constants: Use UPPER_CASE. |
| 51 | + |
| 52 | + Example: |
| 53 | + ```python |
| 54 | + MAX_RETRIES = 3 |
| 55 | + |
| 56 | + def calculate_total(items: list[int]) -> int: |
| 57 | + return sum(items) |
| 58 | + |
| 59 | + class DataProcessor: |
| 60 | + pass |
| 61 | +- Private members should start with a single underscore (_). |
| 62 | +- Exception class names should end in Error. |
| 63 | + |
| 64 | + ```python |
| 65 | + class ValidationError(Exception): |
| 66 | + pass |
| 67 | + ``` |
| 68 | + |
| 69 | +## Import Style |
| 70 | + |
| 71 | +- Imports must be grouped in the following order: |
| 72 | + |
| 73 | + 1. Standard library imports |
| 74 | + |
| 75 | + 2. Third-party imports |
| 76 | + |
| 77 | + 3. Local application imports |
| 78 | + |
| 79 | +- Each group must be separated by one blank line. |
| 80 | + |
| 81 | + Example: |
| 82 | + ```python |
| 83 | + import os |
| 84 | + import sys |
| 85 | + |
| 86 | + from fastapi import FastAPI |
| 87 | + import requests |
| 88 | + |
| 89 | + from app.models import User |
| 90 | + from app.services import user_service |
| 91 | + ``` |
| 92 | + |
| 93 | +- Imports should be one per line. |
| 94 | + |
| 95 | +- Absolute imports should be used. |
| 96 | + |
| 97 | +- Wildcard imports (`from module import *`) should not be used. |
| 98 | + |
| 99 | +## Error Handling |
| 100 | + |
| 101 | +- Specific exceptions should be caught rather than using a bare `except:`. |
| 102 | + |
| 103 | +- When raising exceptions, include an informative error message. |
| 104 | + |
| 105 | +- Exceptions that represent a domain-specific error should subclass `Exception` and be suffixed with `Error`. |
| 106 | + |
| 107 | + Example: |
| 108 | + ```python |
| 109 | + from fastapi import HTTPException |
| 110 | + |
| 111 | + def fetch_data(url: str) -> str: |
| 112 | + try: |
| 113 | + response = requests.get(url) |
| 114 | + response.raise_for_status() |
| 115 | + except requests.RequestException as exc: |
| 116 | + raise HTTPException(status_code=500, detail=str(exc)) |
| 117 | + return response.text |
| 118 | + ``` |
| 119 | + |
| 120 | +## File and Module Structure |
| 121 | + |
| 122 | +- Modules and packages must use short, all-lowercase names. Underscores can be used if necessary. |
| 123 | + |
| 124 | +- Each module must have a single, clear responsibility. |
| 125 | + |
| 126 | + Example project structure: |
| 127 | + ``` |
| 128 | + |
| 129 | + app/ |
| 130 | + ├── main.py |
| 131 | + ├── api/ |
| 132 | + │ ├── users.py |
| 133 | + │ └── items.py |
| 134 | + ├── models/ |
| 135 | + │ └── user.py |
| 136 | + ├── services/ |
| 137 | + │ └── user_service.py |
| 138 | + ├── utils/ |
| 139 | + │ └── helpers.py |
| 140 | + ├── config.py |
| 141 | + |
| 142 | + ``` |
| 143 | +## Constants and Configuration |
| 144 | + |
| 145 | +- Constants must be defined using UPPER_CASE. |
| 146 | + |
| 147 | +- Configuration values should be loaded from environment variables where possible. |
| 148 | + |
| 149 | + Example: |
| 150 | + ```python |
| 151 | + import os |
| 152 | + |
| 153 | + MAX_RETRIES = 5 |
| 154 | + DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db") |
| 155 | + ``` |
| 156 | + |
| 157 | +- Hard-coded configuration values should be avoided. |
0 commit comments