Code Style and Static Analysis⚓
When writing python code, you first need to make sure your code can run (i.e it respects the python language definition). This guide supposes you already know how to write such functional python code.
If you need other people to understand you code (or even yourself in 6 months or so), you need to make sure it's readable:
- we can understand its intent
- we can understand its structure (classes/functions definitions, scopes)
- we can understand how it works (each individual line of code)
Luckily, there are standards in python and tools to check or even enforce them.
Python Enhancement Proposals (PEP)⚓
The Python Enhancement Proposals or PEP are proposals made by python contributors to enhance the python language. It contains proposals adding features to the language (ex: PEP 634 - Structural Patterm Matching: Specification which specifies structural pattern matching which was introduced with python 3.10), proposals describing standards and best practices for python developers to follow, etc.
In this section, we will focus on some standards described in PEPs.
Source: PEP
PEP 8 – Style Guide for Python Code⚓
This style guide stems from the fact that "code is read much more often than it is written". The goal is therefore to make a reasonable effort when writing your code so that users or contributors will have less trouble reading it.
One key component of a readable code is consistency. If the code in your project changes style evry file or class/function definition, it will be very hard to follow as the user will constantly need to adapt to those changes. Hence defining and enforcing a single coding style is very important for the readability of a project's source code.
PEP 8 makes specific recommendations for writing clean python code. Here is a non-exhaustive list:
- Indentation: 4 spaces per indentation level
- Maximum Line Length: limit all lines to a maximum of 79 characters
- Break a Line Before a Binary Operator
- Blank Lines: surround top-level functions and classes with 2 blank lines, surround methods defined inside a class with 1 blank line, use other blank lines sparingly
- Source File Encoding: always use UTF-8, do not declare this encoding
- Imports are always put on top of the file, on separate lines, and are ordered thusly (each section separated by a blank line):
- Standard library imports (for example
time
,os
): all imports from native python libraries - Related third party imports (for example
numpy
,matplotlib
): all imports from external dependencies - Local application/library specific imports: all imports from your own project's code
- Standard library imports (for example
Another important section is the Naming Conventions for your modules, classes and functions.
Note
As the PEP itself admits, it only offers recommendations for writing python code. However, those rules are considered a standard for python developers. Thus, python developers are accustomed to them, and applying other rules for your project, without some very good reason, will make it harder for most developers to understand your code.
Source: PEP 8 – Style Guide for Python Code
PEP 257 – Docstring Conventions⚓
Docstrings are pieces of documentation that you insert within your python code.
They are the first statement in functions, classes, methods or even modules definitions. They become the __doc__
special attribute of that object, and are used to document the object (i.e what it is, what it does, how to use it).
They are surrounded with three double-quotes. There are 2 types of doc-strings:
- One-line Docstrings: They are for obvious cases and should fit in one line
1 2
def function(a, b): """Do X and return a list."""
- Multi-line Docstrings: They consist of a summary line, followed by a blank line, followed by a more elaborate description
1 2 3 4 5 6 7 8 9 10
def complex(real=0.0, imag=0.0): """Form a complex number. Keyword arguments: real -- the real part (default 0.0) imag -- the imaginary part (default 0.0) """ if imag == 0.0 and real == 0.0: return complex_zero ...
In Writing Documentation for Your Software, we will focus in much more details on how to use doc-strings as part of your project documentation.
Source : PEP 257 – Docstring Conventions
PEP 484 – Type Hints⚓
Type hints allow developers to document the types used inside their code. It can be used for functions definition:
1 2 |
|
1 2 3 |
|
When writing those, you should always put the most generic type compatible (e.g for inputs such as function arguments type) and the most precise possible (e.g for outputs such as function return type).
Those type hints are completely ignored at runtime. As such they are used for documentation purposes, to indicates which types are used throughout your code. Furthermore, those type hints can be statically checked for consistency, using tolls such as mypy.
Source: PEP 484 – Type Hints
Linters⚓
A linter is a static analysis tool that checks source code for potential errors, formatting issues, and deviations from best practices or style guides — without executing the program.
Warning
Those linters often focuses on specific code practices, and are thus complementary. However, they are sometimes opinionated on specific topics and will add rules that are sometimes incompatible. If using multiple linters is advised to catch as many badly written code as possible, you will need to setup them in a way they don't clash together.
flake8⚓
🔍 What it does:
flake8
is a lightweight wrapper around pyflakes
, pycodestyle
, and mccabe
. It checks your code for syntax errors, style violations (PEP 8), and complexity.
⚙️ Installation:
1 |
|
🛠️ Basic Usage:
1 |
|
Recommended options:
--max-line-length <n>
: set maximum line length to n (79 is the recommended max line length, see PEP 8)--ignore=<rule_1,rule_2,...>
: ignore the given set of rules (recommend E203,W503 for black compatibility)
🔗 Documentation: https://flake8.pycqa.org
pylint⚓
🔍 What it does:
pylint
is a comprehensive static analysis tool that checks code quality, programming errors, unused variables, bad coding patterns, and enforces a coding standard.
⚙️ Installation:
1 |
|
🛠️ Basic Usage:
1 |
|
You can also generate a config file:
1 |
|
🔗 Documentation: https://pylint.readthedocs.io
ruff⚓
🔍 What it does:
ruff
is an extremely fast Python linter written in Rust. It integrates many tools (flake8
, isort
, pyflakes
, etc.) and supports auto-fixes.
⚙️ Installation:
1 |
|
🛠️ Basic Usage:
1 |
|
To auto-fix issues:
1 |
|
🔗 Documentation: https://docs.astral.sh/ruff
mypy⚓
🔍 What it does:
mypy
is a static type checker for Python. It verifies type annotations and finds type errors without running the code.
⚙️ Installation:
1 |
|
🛠️ Basic Usage:
1 |
|
Options for using external packages without type checking:
--ignore-missing-imports
: ignore imported modules which have no type hinting stubs
Use inline or stub files for type hints. See PEP 484 for inline type hints definition.
🔗 Documentation: https://mypy.readthedocs.io
bandit⚓
🔍 What it does:
bandit
scans your codebase for common security issues in Python code. It’s focused on static analysis for known vulnerability patterns.
⚙️ Installation:
1 |
|
🛠️ Basic Usage:
1 |
|
🔗 Documentation: https://bandit.readthedocs.io
Python Linter Comparison Table⚓
Feature | flake8 | pylint | mypy | ruff | bandit |
---|---|---|---|---|---|
🔍 Purpose | Code style & linting (PEP 8) | Code style & linting (comprehensive) | Static type checking | Linting + formatting + type hints | Security analysis |
🧠 Focus | Syntax, style, complexity | Full static code analysis | Type hints & annotations | Linting + selected checks from others | Common Python security issues |
🛠 Config file | setup.cfg , tox.ini , pyproject.toml |
.pylintrc , pyproject.toml |
mypy.ini , pyproject.toml |
pyproject.toml |
bandit.yaml , CLI args |
⚡ Performance | Moderate (Python) | Slower (Python) | Fast | Very fast (Rust) | Fast |
📦 Install | pip install flake8 |
pip install pylint |
pip install mypy |
pip install ruff |
pip install bandit |
🧪 Dry-run / check mode | Default behavior | Default behavior | Default behavior | ruff check |
Default behavior |
🔁 Auto-fix | ❌ | Limited via plugins | ❌ | ✅ ruff --fix |
❌ |
🧰 Plugin Ecosystem | Large (e.g., flake8-bugbear, flake8-docstrings) | Moderate | Some (e.g., Django plugin) | Built-in rules, no plugins needed | Few |
📈 Output detail | Concise | Verbose with code score | Focused on type errors | Concise, --format options |
Issue summary by CWE category |
🧪 Type Checking | ❌ | Limited | ✅ Static types only | ✅ Basic type checks (subset of mypy ) |
❌ |
🔐 Security linting | ❌ | ❌ | ❌ | ❌ | ✅ |
🤝 Pre-commit support | ✅ | ✅ | ✅ | ✅ | ✅ |
🔗 Docs | flake8 | pylint | mypy | ruff | bandit |
Summary⚓
- flake8: Light, pluggable PEP 8 linter. Ideal for quick, style-focused checks.
- pylint: Deep static analysis, best for full-featured rule checks and reports.
- mypy: Strong typing enforcement, perfect if your codebase uses type hints.
- ruff: Extremely fast all-in-one linter + formatter, replacing flake8, isort, and partially mypy/pylint.
- bandit: Special-purpose linter to catch common Python security flaws in your code.
Code Formatters⚓
Whereas linters are concerned with pointing out bad coding practices, or other static errors, they often don't come up with a way to automatically fix them.
Furthermore, the end goal when setting up coding style for your project, is to be as thorough as possible in the rules you set up so there is only one way to write any piece of code cleanly. This enforces absolute consistency, and formatters are built to tackle that problem.
Tip
Linters and formatters are often complementary. If set up correctly, the code modified by your formatters should always be validated by your linters. And they should not clash with each other.
isort⚓
🔍 What it does:
isort
automatically sorts and organizes your Python import statements by module type and alphabetically. It helps enforce a consistent import order across your codebase.
⚙️ Installation:
1 |
|
🛠️ Basic Usage:
1 |
|
You can also configure it with a .isort.cfg
or pyproject.toml
.
🔗 Documentation: https://pycqa.github.io/isort
black⚓
🔍 What it does:
black
is an uncompromising Python code formatter. It formats entire files in a consistent way according to the Black style, which is based on PEP 8 but with strict rules. You don’t configure the style—Black does it for you.
⚙️ Installation:
1 |
|
🛠️ Basic Usage:
1 |
|
To check formatting without modifying:
1 |
|
🔗 Documentation: https://black.readthedocs.io
ruff (as a formatter)⚓
🔍 What it does:
Besides being a linter, ruff
can also act as a code formatter, replacing both black and isort in many use cases. It offers extremely fast formatting and can handle imports and line length consistently.
⚙️ Installation:
1 |
|
🛠️ Basic Usage (formatting mode):
1 |
|
To auto-format the whole project:
1 |
|
Ruff can be configured in pyproject.toml
(like black and isort).
🔗 Documentation: https://docs.astral.sh/ruff
Python Code Formatter Comparison⚓
Feature | black | isort | ruff |
---|---|---|---|
🔧 Purpose | Code formatting | Sort & group import statements | Code formatting + import sorting + linting |
📦 Scope | Full file formatting | Only imports | Full formatting (can replace black + isort ) |
🛠 Configurable | Minimal | Highly configurable | Moderately configurable |
⚡ Performance | Moderate (written in Python) | Fast (written in Python) | Extremely fast (written in Rust) |
🔁 Supports Fixing | Yes (autoformats code) | Yes (reorders imports) | Yes (autoformats and fixes code) |
🧪 Dry Run / Check Mode | --check |
--check-only |
ruff format --check |
📄 Config File | pyproject.toml |
.isort.cfg / pyproject.toml |
pyproject.toml |
🤝 Pre-commit Support | Yes | Yes | Yes |
🔗 Docs | black.readthedocs.io | isort docs | ruff docs |
Summary⚓
- Use black if you want a strict, no-options, PEP 8-compliant formatter that "just works."
- Use isort if you need to enforce import grouping and sorting.
- Use ruff if you want the fastest, all-in-one tool that combines linting and formatting (including isort and black-style output).
Automatize code style⚓
In this section, we consider you already configured your linters and formatters for executing them manually. You already looked for the options to make them compatible with each other.
If this validation of your code style and practices is only done manually, you will often either forget to execute it or choose not to (seems easier right?). Then this formalization efforts and configuration efforts would have been for nothing, leaving your code to become inconsistent.
To that end, it is better to automatize those validations. Even better, it is better to block commits that don't validate linters or are not formatted properly, so only clean commits are made. This can be done server-side, using Continuous Integration, which we will cover in another guide. But it can either (or better also) be applied locally.
Apply formatting automatically (IDE)⚓
Most IDE, such as VS Code, come with formatting options. If you have set a common IDE and list of plugins/options which any contributor must use to work on your project, you can then use your IDE configuration to choose formatting options.
If this is not the case though, we recommend you first manually test and select, both formatting tools and options, and simply set your IDE to apply them. For VS Code, you can install the File Watchers extension, and make it run a bash command executing your formatting tools.
Tip
Doing it like this allow you to reuse the same configuration of formatters for local execution (via VS Code File Watchers, or even manual execution) and for Continuous Integration. So you know that your IDE will format code that won't clash with the remote repository rules.
For File Watchers, please find below a VS Code .vscode/settings.json
snippet, which will run isort and black, with compatibility options for the flake8 linter, everytime a .py
file is modified:
1 2 3 4 5 6 7 8 9 10 11 |
|
Note
This command will apply formatting to every source files whenever one is modified.
If your project becomes very large, you may need to tweak it so it only apply to the modified file in question. The ${workspaceRoot}
variable is a VS Code default variable corresponding to the path to your current workspace (e.g the root of your project).
The advantage of setting automatic formatting of your code is that you don't have to think about it as you code. You can literally write your code in a quick and dirty way, save and it will become clean immediately.
Pre-commit hook⚓
Another, complementary, approach is to perform linting & formatting checks before making a commit, blocking it if they fail. This can be accomplished automatically using a pre-commit hook.
This hook will run everytime you call the git commit
command, and will block it if the hook fails.
Here is an example of such a hook, applying flake8 linter, isort and black formatters in validation mode. It must be called .git/hooks/pre-commit
and be executable:
1 2 3 4 5 6 |
|
Tip
This hook will block commit that don't pass those checks, but it won't return the logs showing the reasons. You can modify it though, so the logs won't show if the checks pass (so your commit will just work normally), but they will if they fail, providing you with feedback:
1 2 3 4 5 6 7 8 9 10 |
|