Ruff v0.9.0 is available now! Install it from PyPI, or your package manager of choice:

uv 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.

With a new year comes a new Ruff minor release! Among other changes, this release brings with it a new style guide for the Ruff formatter, seven lint rule stabilizations, and two stabilized lint rule autofixes.

The Ruff 2025 style guide

This release ships a new style guide for the Ruff formatter. Significant changes include:

  • Interpolated expressions inside f-strings are now formatted
  • Single-line implicitly concatenated strings are now avoided where possible
  • Improved compatibility with Black’s style guide

For a detailed list of all style changes, head to the release page.

F-String formatting

With the new style guide, Ruff now formats expressions interpolated inside f-string curly braces:

--- 2024
+++ 2025

     return {
         "statusCode": 200,
-        "body": json.dumps({"message": f"{layer_method()+5}"}),
+        "body": json.dumps({"message": f"{layer_method() + 5}"}),
     }

Otherwise, however, Ruff now formats the f-string like any other regular string:

  • F-string quotes are now normalized according to your project's configuration. Quotes will still be left untouched, however, if normalizing them would result in an invalid f-string or would require escaping more quotes in the f-string’s literal parts.
  • Unnecessary escapes are now removed from inside f-strings. For example, \' will be replaced with ' if the f-string uses " for its outer quotes.
--- 2024
+++ 2025

  assert (dag_id in actual_found_dag_ids) == should_be_found, (
-     f'dag \'{dag_id}\' should {"" if should_be_found else "not "}'
+     f"dag '{dag_id}' should {'' if should_be_found else 'not '}"
      f"have been found after processing dag '{expected_dag.dag_id}'"
  )

Lastly, Ruff now looks at the interpolated expressions (the parts inside {} curly braces) in an f-string to see whether it might be acceptable to split the f-string over multiple lines. If any interpolated expression in the f-string contains a line break (either using a multiline expression or a comment inside the braces), Ruff will now sometimes add other line breaks to the f-string if it would help wrap the string to your project's maximum line length.

# Source
f"Unsupported serializer {serializer!r}. Expected one of {', '.join(map(repr, _SERIALIZERS))}"

# Add a line break manually to inform Ruff to split the f-string...
f"Unsupported serializer {
    serializer!r}. Expected one of {', '.join(map(repr, _SERIALIZERS))}"

# 2025 style guide: Ruff now splits the f-string as it does not fit the line length limit,
# and it sees from the line break that this would be acceptable
f"Unsupported serializer {serializer!r}. Expected one of {
    ', '.join(map(repr, _SERIALIZERS))
}"

For more details on how Ruff now formats f-strings, see the documentation.

Fewer single-line implicitly concatenated strings

Implicitly concatenated strings can be a convenient way to breaking long strings into multiple shorter literals. However, they are also easily overlooked and can sometimes reduce readability, especially when the concatenated literals are placed next to each other on a single line.

With the new style guide, Ruff merges the implicitly concatenated strings if they all fit on a single line:

--- 2024
+++ 2025

  raise argparse.ArgumentTypeError(
-     f"The value of `--max-history {max_history}` " f"is not a positive integer."
+     f"The value of `--max-history {max_history}` is not a positive integer."
  )

All implicitly concatenated strings receive this treatment, except:

  • Triple-quoted strings ('''string''')
  • Raw strings (r"string")
  • Some rare combinations of f-strings with debug texts containing quotes or format specifiers containing quotes (e.g. f'{1=: "abcd \'\'}' or f'{x=:a{y:{z:"abcd"}}}')

For strings that can’t be automatically joined, Ruff now preserves whether the implicitly concatenated string literals are formatted over multiple lines or on a single line.

# Source
single_line = r'^time data "a" doesn\\'t match format "%H:%M:%S". ' f"{PARSING_ERR_MSG}$"
multi_line = (
    r'^time data "a" doesn\\'t match format "%H:%M:%S". '
    f"{PARSING_ERR_MSG}$"
)

# 2024 style guide: the multiline implicitly concatenated string was converted
# to a single-line implicitly concatenated string
single_line = r'^time data "a" doesn\\'t match format "%H:%M:%S". ' f"{PARSING_ERR_MSG}$"
multi_line = r'^time data "a" doesn\\'t match format "%H:%M:%S". ' f"{PARSING_ERR_MSG}$"

# 2025 style guide: neither string can be easily joined
# to remove the implicit concatenation,
# so both are left as they are in the source
single_line = r'^time data "a" doesn\\'t match format "%H:%M:%S". ' f"{PARSING_ERR_MSG}$"
multi_line = (
    r'^time data "a" doesn\\'t match format "%H:%M:%S". '
    f"{PARSING_ERR_MSG}$"
)

Preserving the source formatting for these cases means that the longstanding formatter incompatibility with our ISC001 rule is now resolved. Users will no longer see a warning if they have both the formatter and this lint rule enabled in their Ruff configuration.

Prefer wrapping the assertion message

If an assert statement is too long to fit onto a single line, Ruff has two possible choices. It could either split the expression (the first part of the statement) over multiple lines, or it could split the assertion message (the part of the statement following the comma).

Previously, Ruff preferred breaking the assert statement’s expression when it encountered an assert statement longer than the maximum line length. This had the advantage that it often avoided the need for additional parentheses around the assertion message. Nonetheless, the Ruff 2025 style guide prefers to parenthesize the assertion message rather than breaking the assertion expression over multiple lines. We think that the assertion expression is the most important part of an assert statement; keeping it on a single line where possible outweighs other considerations and improves readability:

--- 2024
+++ 2025

- assert (match_opt1 is not None) or (
-     match_opt2 is not None
- ), "Cannot find paper abstract section!"
+ assert (match_opt1 is not None) or (match_opt2 is not None), (
+     "Cannot find paper abstract section!"
+ )

This new formatting aligns with Ruff's assignment formatting, where it prefers splitting the value on the right-hand side of an assignment before it will resort to parenthesizing the assignment's targets on the left-hand side.

Fewer parenthesized return annotations

Previously, Ruff often added parentheses around multiline or subscript return-type annotations when a function declaration exceeded the maximum line length. Ruff now avoids adding parentheses in cases where the return type comes with its own set of parentheses or is known to be multiline.

--- 2024
+++ 2025

- def _register_filesystems() -> (
-     Mapping[
-         str,
-         Callable[[str | None, Properties], AbstractFileSystem]
-         | Callable[[str | None], AbstractFileSystem],
-     ]
- ):
+ def _register_filesystems() -> Mapping[
+     str,
+     Callable[[str | None, Properties], AbstractFileSystem]
+     | Callable[[str | None], AbstractFileSystem],
+ ]:
      scheme_to_fs = _BUILTIN_SCHEME_TO_FS.copy()

The new formatting now aligns more closely with Black’s return-type annotation formatting.

Automatically parenthesized if guards in match case statements

Previous versions of Ruff never added parentheses to if guards in case clauses of match statements. Unnecessary parentheses, meanwhile, were left untouched. The former could result in formatting that was hard to read due to case patterns being unnecessarily split over multiple lines. The latter could result in unnecessarily parenthesized if guards with a large amount of visual noise.

Ruff now automatically parenthesizes an if guard when a case line goes beyond its maximum allowed length according to your project's configuration. This has the added effect that the if guards are broken before the case pattern, improving readability:

--- 2024
+++ 2025

  # Options: line-length = 60

  match command.split():
-     case [
-         "go",
-         direction,
-     ] if direction in current_room.exits:
+     case ["go", direction] if (
+             direction in current_room.exits
+     ):
          current_room = current_room.neighbor(direction)
      case ["go", _]:
          print("Sorry, you can't go that way")
-     case ["wait", _] if (current_room.waiting_room):
+     case ["wait", _] if current_room.waiting_room:
          pass

Rule stabilizations

The following rules have been stabilized and are no longer in preview:

Other behavior stabilizations

The following rules now have improved autofix behavior that was previously only available in preview mode:

And finally, Ruff v0.9 stabilizes several other behaviors that were previously only available to users who opted into preview:

Thank you!

Thank you to everyone that provided feedback regarding the formatter and other changes included in Ruff's --preview mode, and especially, to our contributors. It's an honor building Ruff with you!


View the full changelog on GitHub.

Read more about Astral — the company behind Ruff.

Thanks to Alex Waygood and Dhruv Manilawala, who both contributed to this blog post.