Ruff v0.1.8 is now available with opt-in support for formatting Python code examples in docstrings. Install it from PyPI, or your package manager of choice:

pip install --upgrade ruff

As a reminder: Ruff is an extremely fast Python linter and formatter, written in Rust. Ruff can be used to replace Black, Flake8 (plus dozens of plugins), isort, pydocstyle, pyupgrade, and more, all while executing tens or hundreds of times faster than any individual tool.

View the full changelog on GitHub, or read on for the highlights.

Formatting code snippets in docstrings

We recently released the Ruff formatter, an extremely fast Python code formatter.

In v0.1.8, the formatter gains the ability to format Python code snippets in docstrings, with support for the following formats:

  • The Python doctest format.
  • CommonMark fenced code blocks with the following info strings: python, py, python3, or py3. Fenced code blocks without an info string are assumed to be Python code examples and, as such, are also formatted.
  • reStructuredText literal blocks. While literal blocks may contain things other than Python, this is meant to reflect a long-standing convention in the Python ecosystem where literal blocks often contain Python code.
  • reStructuredText code-block and sourcecode directives. As with Markdown, the language names recognized for Python are python, py, python3, or py3.

Here's a quick example. First, we'll create a ruff.toml configuration file with the following contents:

[format]
# Docstring formatting is opt-in. Enable it by setting this option to `true`.
docstring-code-format = true

Second, we'll create a Python file, sample.py. We'll use a number of different formats in one docstring to demonstrate ruff format's broad support:

def f(x):
    """
    Something about `f`. And an example in doctest format:

    >>> f( '''can't fool me'''
    ... )

    Markdown is also supported:

    ```py tab="plugin.py"
    foo, bar, quux = this_is_a_long_line(lion, hippo, giraffe, zebra, penguin, lemur, bear)
    ```

    Fenced code blocks without labels are assumed to be Python:

    ```
    f(  x  )
    ```

    As are reStructuredText literal blocks::

        f(  x  )

    And reStructuredText code blocks are also supported:

    .. code-block:: python
        :linenos:

        def definitely_converges(n):
            if n%2==0:
                return n//2
            else:
                return 3*n+1

    Code examples that aren't valid Python get skipped automatically::

        println!( "Don't mistake me for Python" );
    """
    pass

Finally, we'll run it through ruff format:

ruff format --config ruff.toml sample.py

On completion, sample.py should look like:

def f(x):
    """
    Something about `f`. And an example in doctest format:

    >>> f('''can't fool me''')

    Markdown is also supported:

    ```py tab="plugin.py"
    foo, bar, quux = this_is_a_long_line(
        lion, hippo, giraffe, zebra, penguin, lemur, bear
    )
    ```

    Fenced code blocks without labels are assumed to be Python:

    ```
    f(x)
    ```

    As are reStructuredText literal blocks::

        f(x)

    And reStructuredText code blocks are also supported:

    .. code-block:: python
        :linenos:

        def definitely_converges(n):
            if n % 2 == 0:
                return n // 2
            else:
                return 3 * n + 1

    Code examples that aren't valid Python get skipped automatically::

        println!( "Don't mistake me for Python" );
    """
    pass

Code snippets that aren't valid Python won't be formatted. Similarly, if ruff format formatter would produce an invalid Python program after formatting, then it will also silently skip the code snippet.

Users may also configure the line length limit used for formatting Python code snippets in docstrings. The default, dynamic, instructs the formatter to respect the line length limit setting for the surrounding Python code. dynamic ensures that even when code snippets are found inside indented docstrings, the line length limit configured for the surrounding Python code will be respected.

Users may also configure a fixed line length limit for code snippets in docstrings, which can be useful for ensuring that code snippets in generated documentation adhere to a consistent line length irrespective of the degree of indentation in the originating source code.

For example, this sets the line length limit to 20:

[format]
docstring-code-format = true
docstring-code-line-length = 20

And here's our sample.py to test it out:

def g(x):
    """
    Another example demonstrating how long lines get wrapped.

    >>> foo, bar, quux = this_is_a_long_line(lion, hippo, giraffe, zebra, penguin, lemur, bear)

    """
    pass

Now run ruff format again:

ruff format --config ruff.toml sample.py

And sample.py should now look like this:

def g(x):
    """
    Another example demonstrating how long lines get wrapped.

    >>> (
    ...     foo,
    ...     bar,
    ...     quux,
    ... ) = this_is_a_long_line(
    ...     lion,
    ...     hippo,
    ...     giraffe,
    ...     zebra,
    ...     penguin,
    ...     lemur,
    ...     bear,
    ... )

    """
    pass

Docstring snippet formatting is opt-in, but will be made opt-out in a future release.

Auto-quoting of type annotations

In a past release, Ruff gained the ability to identify imports whose bindings are only used in type annotations, and automatically move those imports into if TYPE_CHECKING: blocks. See, for example, typing-only-third-party-import. Moving imports into type-checking blocks is a common pattern in Python to avoid circular imports and reduce program startup time.

To-date, this rule has been hindered by the fact that Python requires most type annotations to be accessible at runtime. For example, the following snippet will fail at runtime, as Python needs to evaluate Sequence[int] in order to add it to the module's __annotations__ field:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # Sequence is only available to type-checkers, and not at runtime...
    from collections.abc import Sequence


# But Python needs to evaluate `Sequence[int]` here.
def func(value: Sequence[int]) -> None:
    ...

A common solution here is to add from __future__ import annotations to the top of the file, which instructs Python to evaluate type annotations lazily. However, adding from __future__ import annotations is a significant semantic change, and can lead to other problems.

In v0.1.8, Ruff gains the ability to automatically quote such type annotations, if doing so would allow the import to remain or be moved into a type-checking block. For example, Ruff can now automatically rewrite the above snippet to:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Sequence


def func(value: "Sequence[int]") -> None:
    ...

This behavior is opt-in via the quote-annotations setting, and has no effect when from __future__ import annotations is present:

[flake8-type-checking]
quote-annotations = true

Contributors

A sincere thank-you to those that contributed to this release: