Skip to content

4.5. Formatting

What is code formatting?

Code formatting is the practice of applying a consistent style guide to your source code. It governs aesthetic aspects of code, such as indentation, line length, variable naming, and the placement of comments. The goal is to make the code uniform and predictable, improving its overall quality and readability.

Why is code formatting crucial?

Consistent formatting is a cornerstone of professional software development for several key reasons:

  1. Improves Readability: Well-formatted code is visually organized, making it easier for developers to read, understand, and navigate complex logic.
  2. Streamlines Collaboration: When all team members adhere to the same formatting rules, it eliminates stylistic inconsistencies. This ensures that the codebase feels familiar to everyone, reducing friction and onboarding time.
  3. Enhances Maintainability: A uniform style makes it easier to spot bugs, apply updates, and refactor code. It also prevents trivial debates (e.g., tabs vs. spaces), allowing the team to focus on solving real problems.

What is the standard formatting convention for Python?

The official style guide for Python code is PEP 8. It provides a comprehensive set of guidelines for everything from code layout to naming conventions. Adhering to PEP 8 is highly recommended as it is the universal standard across the Python community.

While it's best to stick to the defaults, some PEP 8 rules can be adjusted. For example, the default line length is 79 characters, which was suitable for older monitors. On modern screens, a value like 88 or 100 is often more practical.

What is the difference between formatting and linting?

Although often used together, formatting and linting serve different purposes:

  • Formatting automatically rewrites your code to conform to a specific style guide. Its primary goal is to ensure visual consistency and readability. It is non-discretionary and deterministic.
  • Linting analyzes your code to detect programmatic errors, potential bugs, stylistic issues, and "code smells." Its primary goal is to improve code quality and prevent errors. It flags issues but often requires manual intervention to fix them.

Tools like Ruff can perform both formatting and linting, providing a comprehensive solution for code quality.

Which tools should you use to format a Python codebase?

While black (a code formatter) and isort (an import sorter) were the traditional choices, Ruff now provides a superior, all-in-one solution. Ruff is an extremely fast formatter and linter that can replace both black and isort, simplifying your toolchain.

You can install and run Ruff to format your entire codebase with these commands:

# Install Ruff into your "dev" dependency group
uv add --group dev ruff

# Sort and organize all import statements
uv run ruff check --select I --fix src/ tests/

# Format all source code files
uv run ruff format src/ tests/

The MLOps Python Package wires exactly these two steps to a single mise run format:python task, adding --force-exclude so the formatter honors its exclusions even when a git hook hands it explicit file paths:

uv run ruff check --select=I --fix --force-exclude . && uv run ruff format --force-exclude .

What does Ruff 0.16 format that earlier versions did not?

Since Ruff 0.16.0 (released 2026-07-23), ruff format also formats Python code blocks inside Markdown files by default. A fenced block tagged python in your README.md or your documentation is now reformatted like any .py file:

```python
x  =  1
```

becomes:

```python
x = 1
```

Two practical consequences follow:

  1. Your documentation examples stay correct. Code snippets in READMEs drift stylistically from the code they illustrate, because nothing used to check them. Now they are held to the same standard as the source.
  2. Your Ruff version floor has to move with it. A repository formatted by 0.16 fails ruff format --check under 0.15, because the older binary leaves those Markdown blocks alone and reports the newer output as unformatted. Declare the floor explicitly, as the reference package does, and explain it in the same place:

    [dependency-groups]
    dev = [
      # Ruff 0.16 formats Python inside Markdown and rewrote the default rule set:
      # an older Ruff would disagree with this repository's formatting, so floor it.
      "ruff>=0.16.2",
    ]
    

The same release rewrote Ruff's default lint rule set, from 59 rules to 413. That change is covered in 4.1. Linting.

How can you format configuration and documentation files?

Ruff only formats Python (including, now, the Python it finds inside Markdown). A real project also contains JSON, Markdown, TOML, and YAML files (pyproject.toml, mise.toml, GitHub workflows, this very documentation), and those deserve the same consistency. dprint is a fast, pluggable formatter that handles exactly these config and markup formats, complementing Ruff on the Python side.

# Install dprint (for example with mise, or see https://dprint.dev/install/)
mise use dprint

# Format all JSON, Markdown, TOML, and YAML files in place
dprint fmt

# Check formatting without modifying files (ideal for CI)
dprint check

You enable formats and tune behavior through a dprint.jsonc file at the project root. Each format is provided by a versioned plugin, which keeps results reproducible across machines:

{
    "$schema": "https://dprint.dev/schemas/v0.json",
    "lineWidth": 120,
    "excludes": [
        "**/*-lock.json",
        "**/node_modules",
        "**/references/**",
        ".git",
        ".venv"
    ],
    "markdown": {
        "textWrap": "never"
    },
    "plugins": [
        "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.6.0.wasm",
        "https://plugins.dprint.dev/json-0.23.0.wasm",
        "https://plugins.dprint.dev/markdown-0.22.1.wasm",
        "https://plugins.dprint.dev/toml-0.7.0.wasm"
    ]
}

Do Ruff and dprint fight over Markdown?

No, and the reason is worth understanding, because both tools now touch .md files.

dprint's Markdown plugin formats the document: headings, list markers, tables, emphasis. It can also format the contents of a fenced code block, but only when a plugin for that block's language is loaded. The configuration above declares four plugins — YAML, JSON, Markdown, TOML — and no Python plugin exists in that list, so dprint reads a python-tagged fence, finds no formatter for it, and copies the block through untouched.

Ruff does the mirror image: it formats the Python inside those fences and ignores the prose around them.

The two therefore partition the file cleanly, with no rule in common and no last-writer-wins race. That is why mise run format can run them in sequence without either undoing the other:

# format:python — Ruff sorts imports, then formats .py files and Python-in-Markdown
uv run ruff check --select=I --fix --force-exclude . && uv run ruff format --force-exclude .

# format:dprint — dprint formats JSON, Markdown prose, TOML, YAML
dprint fmt

With Ruff and dprint, a single task can format the entire repository. This is exactly why the canonical mise vocabulary splits mise run format into format:python (Ruff) and format:dprint (dprint), while mise run check:format runs ruff format --check and dprint check so continuous integration fails on any unformatted file.

How can you automate formatting?

Manually running commands is inefficient. The best practice is to configure your code editor to format your code automatically every time you save a file. This "set it and forget it" approach ensures your code is always compliant without any extra effort.

For VS Code, you can install the Ruff extension and add the following to your [project].code-workspace file:

{
    "settings": {
        // Enable format on save for all files
        "editor.formatOnSave": true,
        // Specific settings for Python files
        "[python]": {
            // Run code actions like organizing imports on save
            "editor.codeActionsOnSave": {
                "source.organizeImports": "explicit"
            },
            // Set Ruff as the default formatter for Python
            "editor.defaultFormatter": "charliermarsh.ruff",
        },
    },
    "extensions": {
        // Recommend the Ruff extension to anyone opening the project
        "recommendations": [
            "charliermarsh.ruff",
        ]
    }
}

When should you customize formatting rules?

To maximize productivity and avoid style debates, it is highly recommended to adopt the default settings of your chosen formatter. This is often called the "zero-configuration" principle.

However, if your project requires specific adjustments, you can configure Ruff in your pyproject.toml file. Common customizations include line length and docstring conventions.

[tool.ruff]
# Set the maximum line length
line-length = 120

[tool.ruff.format]
# Enable formatting of code snippets within docstrings
docstring-code-format = true
# Always write LF endings, so Windows checkouts do not churn the diff
line-ending = "lf"
# Prefer double quotes (the default, stated explicitly for readers of the config)
quote-style = "double"

[tool.ruff.lint.pydocstyle]
# Set the expected docstring style (e.g., google, numpy)
convention = "google"

How can you disable formatting for specific lines?

On rare occasions, you may need to prevent the formatter from altering a specific block of code where the default formatting reduces readability. You can achieve this in two ways:

  • Implicitly: Add a trailing comma inside a list, dictionary, or set to force the formatter to keep each item on a separate line.

    # The trailing comma prevents this dict from being collapsed into one line
    items = {
        "a": 1,
        "b": 2,
        "c": 3,
    }
    
  • Explicitly: Wrap the code block with # fmt: off and # fmt: on comments to tell the formatter to ignore it completely.

    # fmt: off
    # This block will not be formatted
    not_formatted      = 3
    also_not_formatted = 4
    # fmt: on
    

Additional Resources