All of our Python code should match the PEP8 style guide and the Django coding style guidelines.
We follow PEP8's line length instead of Django's for consistency with our other development standards. We follow Django's recommended import ordering in Django projects.
We use Black to standardize formatting of our Python code and make linting easier. We override the default Black line length to 79 to match PEP8. We also exclude generated files like migrations. We provide a sample pyproject.toml
file containing line length and exclude configuration for Black.
For linting we recommend flake8, and provide a sample flake8 configuration. Exceptions to PEP8 are found in that sample flake8 configuration, and include:
E731
, we allow assignment oflambda
expressionsW503
, we allow line breaks after binary operatorsW504
, we allow line breaks before binary operators
On the whole, generated Python files like Django migrations can be safely ignored.
The flake8 and configuration can go into a tox.ini
file under a [flake8]
header.
It is highly recommended to configure flake8 linting in your editor of choice to use the configuration file above or one provided in the project you're working on.
See Imports in PEP8 and Imports in Django Coding Style.
We use isort to lint imports to comply with both style guidelines, and provide a sample isort configuration. This configuration attempts to enforce the following conventions:
- Multiple-line
from
imports become grouped within parenthesis with one imported name per-line and a trailing comma for the last imported name. - Django imports are included as their own section between the standard library and third-party imports.
- For Wagtail-specific projects like consumerfinance.gov, Wagtail imports are included as their own section between Django and third-party imports.
The isort configuration can go into a tox.ini
file under an [isort]
header.
We recommend using tox for matrix testing against our supported versions of our core dependencies, and we provide a sample tox configuration that tests against both our current production the latest versions Python and Django.
When dealing with support for multiple versions of Python or major dependencies, we prefer to use feature detection instead of version detection for incompatibilities/breaking changes that may exist between versions.
See Tracking Django and Wagtail for more detailed guidance.
The difference between pinned versions of dependencies in requirements.txt
and dependencies specified in setup.py
is frequently confusing and often misunderstood. The difference is between concrete and abstract dependencies, which is most easily understood in terms of applications (intended to be deployed and run), which specify concrete dependencies, and libraries (reusable code that may be included in applications), which specify abstract dependencies.
Donald Stufft has an excelent article with a more in-depth discussion of this difference. For our purposes, we use both requirements files and requirements specified in setup.py
depending on the circumstance of the dependency.
Our applications should use requirements files with pinned versions. For example, consumerfinance.gov has several requirements files that pin all relevant versions for deployment, testing, local execution, and other specific needs. Some of these requirements files inherit from each other (e.g., base.txt
uses the -r libraries.txt
directive to include the requirements from that file).
Our libraries should specify dependencies with supported version ranges. The minimum version in the range and the last available version should be tested with tox and can be major, minor, or patch). The maximum version that is expected to break compatibility can be a theoretical future major. A tox test environment should be included that pulls in the latest version within that range and tests it. The range should be specified in setup.py
using the appropriate setuptools _requires
keyword. For example, our wagtail-sharing library declares install-time requirements with install_requires
and testing requirements with extras_require={'testing': testing_extras}
.