Python style guide
This manual is designed to aid developers in writing Python code that is clear and consistent, within, and across, projects at GDS.
Code formatting
We use Ruff to format our code. Ruff aims for cross compatibility with the popular opionionated formatter Black. Where Ruff and PEP 8 do not express a view (for example, on the usage of language features such as metaclasses) we defer to the Google Python style guide. Use these as references unless something is explicitly mentioned here. These rules should be followed in conjunction with the advice on consistency on the main programming languages manual page.
If you want to add a new rule or exception please create a pull request against this repo.
Maximum line length of 120 characters
PEP 8 dictates a preferred maximum line length of <= 79. This is is a hangover from developing in a Unix terminal window. The vast majority of developers are now using an IDE which can handle a greater line length.
Couple this with the fact that much of the time GDS developers are coding web apps and
have to deal with nested JSON objects, ORM model definitions/ queries, and error/ url
strings and this convention begins to show its age.
Linting
Ruff
This manual advises the use of the Ruff command line checker as an all in one formatter, linter, codestyle and complexity checker.
How to use Ruff
First you should add the Ruff module (available from PyPI) to your ‘dev’ or ‘test’ requirements/dependencies.
You’ll then likely want to run it alongside your unit tests.
Ruff ignores
Ruff can ignore particular lines or files.
A particularly useful feature of ruff is the ability to specify rule exemptions per directory or file.
Commonly it’s used for ignoring unused imports in module level __init__.py
files or imports not being at the top of a file in settings files or scripts.
The feature is documented in the Ruff documentation, under per-file-ignores. You can also see an example in the Notifications API repo.
Common Configuration
Notify is already running the latest verions of Ruff on all
of its repos. You can find an example of their configuration in the root of any
repo in the ruff.toml file.
line-length = 120
target-version = "py311"
select = [
"E", # pycodestyle
"W", # pycodestyle
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C90", # mccabe cyclomatic complexity
"G", # flake8-logging-format
]
ignore = []
exclude = [
"migrations/versions/", ...
]
In the above file we exclude directories we want the checker to ignore completely, include optional linting such as applying isort rules to ensure imports are sorted for minimal git diffs, set the maximum line length and set the target python version.
Note: you can also ignore rules on particular lines of code or files by adding a # noqa
comment - see ruff’s noqa syntax.
Additional linting resources
- Notify Config: A production config to base off
- Ruff error codes list
Environments
This manual advises the use of uv to manage different versions of Python you have installed.
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
direnv is used to manage environment variables.
This ensures project specific variables do not clutter your main environment or the environment of other projects.
direnv uses .envrc for general project specific variables and you can use a non version controlled .secrets to store sensitive information.
We recommend you put source .venv/bin/activate in your .envrc file to ensure your virtual environment is always activated.
Dependencies
Dependency manager
You should use uv. This is recommended because it is fast, modern, aligned with standards like pyproject.toml and is well-maintained. Prefer it to pip, poetry and pipenv.
Specifying dependencies
You should use pyproject.toml to specify your direct dependencies because it is the modern, recommended standard.
Pinning and lockfiles
You must pin your dependencies with a lock-file (unless it’s a library - see Libraries) for example uv.lock.
The lockfile must store a cryptographic hash of the package, and check the hash on install, to guard against installing tampered packages.
The lockfile must be used whenever installing/deploying packages during build or deployment (or some other non-development environment), to maintain consistency between environments.
If you’re using uv this probably means using uv run --frozen or uv sync --frozen.
Note: The above uv commands work differently to pip (e.g.
uv pip sync):
uv runanduv synchave situations where they update the lockfile, re-resolving the dependencies (unless you use--frozen). The uv developers have explained the default favours convenience during local development over build / deploy.
Updating
See the overall advice in: How to manage third party software dependencies. The remainder of this section covers Python-specific practicalities.
Dependabot supports uv - see Using uv with Dependabot. It will bump dependencies in pyproject.toml and regenerate uv.lock.
Libraries
Libraries are the exception to the rule about exact dependencies being specified and pinned. This applies to any Python repository that intended to be installable (into a virtual environment, a container, or onto bare metal) as a dependency of some larger system or application. It may be applicable to repositories that provide scripts to be run by developers or other end-users, but is not recommended for code that’s intended to be deployed on its own into the cloud.
Put version ranges in your library’s pyproject.toml.
For each of your library’s dependencies you’ll need to identify the range of versions likely to be compatible:
The range you choose will depend on the guarantees each dependency makes about backward-compatibility. For example, if you’re currently using version 1.3.1 of a semantically-versioned library, it would be reasonable to specify a range such as
<2.0,>=1.3.1. However, for a library that does not make that guarantee, you might specify a more restricted range, such as<1.4,>=1.3.1.Update this file whenever you are ready to test and validate a new version that falls outside the existing range.
If you have dependencies that are not available on PyPI (for example, because you’ve fixed a bug by forking the code), then you can use a PEP 440 git reference in your
install_requireslist.
Specify dependencies needed only for testing your library in tox.ini if you are using Tox
(example),
or in a requirements-dev.txt (example)
file otherwise.