Skip to content

Code Style

Roman Ludwig edited this page Jun 26, 2025 · 3 revisions

This guide summarizes our Python code style conventions.

General Style and Linting

Generally, we follow PEP 8. Additionally, we use ruff as code linter and formatter. When configured as pre-commit hook, as we typically do in our development setup, ruff enforces a predefined (and quite long) list of rules before every commit. Many of these rules are based on PEP 8, but often go beyond that and e.g. also catch common bugs and anti-patterns.

For example, rule B006 will warn of using mutable data structures (like lists or dictionaries) as default values for function arguments, which is a common mistake in Python code. ruff will also automatically format the code according to its rules, so you don't have to worry about formatting issues.

In most projects, we use the following configuration in our pyproject.toml file to select the rules we want to enforce:

If you have ruff also installed in your editor, it will automatically highlight any issues in your code as you type, making it easy to fix them even before committing. Also, one can learn quite a bit about writing better Python code by occasionally reading the explanation for a particular rule.

[tool.ruff.lint]
select = ["E", "F", "W", "B", "C", "R", "U", "D", "I", "S", "T", "A", "N", "COM", "FURB", "NPY", "UP"]

Docstrings

Every public function and method must include a docstring. The docstring should begin with a concise one-line summary, optionally followed by a more detailed description. Rather than using a separate Args section, mention every argument and its effect directly within the narrative, using double backticks (``parameter_name``) for parameter names and other inline code. For cross-references to other symbols, use Sphinx roles such as :py:class:`SomeClass` to allow users to read up on related stuff. Whenever possible, include doctests to demonstrate usage and expected behavior.

Here's a (maybe contrived) example of a well-structured docstring:

def process_order(
    order_id: int,
    items: list[float],
    discount: float = 0.0,
    expedited: bool = False
) -> dict[int, float]:
    """Process an order and return a summary with totals.

    Calculates the total cost of the order with ``order_id``, by summing
    up the prices in ``items``, then applying a ``discount`` and adding
    a fixed fee if the shipping should be ``expedited``.

    Returns a dictionary with the order ID as the key and the total
    cost as the value.

    >>> process_order(
    ...     order_id=123,
    ...     items=[10.0, 20.0],
    ...     discount=0.1,
    ...     expedited=True,
    ... )
    {123: 42.0}
    """
    total = sum(items)
    total -= total * discount

    if expedited:
        total += 15.0

    return {order_id: total}

It doesn't list the parameters and duplicates the information of their types, but every arguments is mentioned as part of the explanation. This is our preferred style, as it avoids redundancy and keeps the docstring concise.

Type Hints

We try to use type hints wherever possible for function arguments and return values, because type hints improve readability and help with editor support, but we do not require type checking as part of CI or development.

For example, in the function above, type hints help to better understand what the function does (and of course, they also help with editor support). In this (somewhat contrived) example, it would be entirely possible that discount is a boolean, but given the type hint, we already know it's either a percentage or an absolute discount value to be subtracted from the total cost.

Clone this wiki locally