Setting the 2026 stable style
2023: #3407 2024: #4042 2025: #4522 Ruff: https://github.com/astral-sh/ruff/issues/20482
As always, the ideal is for all preview features to be stabilized, not including the unstable ones.
Ordered by most controversial to least:
multiline_string_handling (#1879)
Make expressions involving multiline strings more compact.
Input
textwrap.dedent(
"""\
This is a
multiline string
"""
)
MULTILINE = """
foobar
""".replace(
"\n", ""
)
Output (off) unchanged
Output (on)
textwrap.dedent("""\
This is a
multiline string
""")
MULTILINE = """
foobar
""".replace("\n", "")
Fairly controversial. There were some issues in #4159 that were never fully fixed. Additionally, it's only been in preview since v25.11 (but the issues were resolved as-best-as-possible since v25.9). However, we haven't gotten any further feedback since then. I think the current state is an improvement overall and we probably won't be able to do much else. IMO, we should at least include it in the first v26 beta to get user feedback, even if we don't plan on stabilizing it.
wrap_long_dict_values_in_parens (#3440)
Add parentheses around long values in dictionaries.
Input
my_dict = {
"a key in my dict": a_very_long_variable * and_a_very_long_function_call() / 100000.0
}
Output (off) unchanged
Output (on)
my_dict = {
"a key in my dict": (
a_very_long_variable * and_a_very_long_function_call() / 100000.0
)
}
Could cause a lot of churn. Adds more parentheses and lines to reduce line length. Deferred in 2023 and 2024, then moved to unstable until 25.1, when the issues with it were resolved and it was moved to preview. Ruff has already decided this to be a non-priority (https://github.com/astral-sh/ruff/issues/12856, #4123).
wrap_comprehension_in (#4699)
Wrap the in clause of list and dictionary comprehensions across lines if it would otherwise exceed the maximum line length.
Input
[a for graph_path_expression in refined_constraint.condition_as_predicate.variables]
Output (off)
[
a
for graph_path_expression in refined_constraint.condition_as_predicate.variables
]
Output (on)
[
a
for graph_path_expression in (
refined_constraint.condition_as_predicate.variables
)
]
Could cause a lot of churn. Adds more parentheses and lines to reduce line length. Ruff has already decided this to be a non-priority (#4123).
remove_parens_from_assignment_lhs (#4865)
Remove unnecessary parentheses from the left-hand side of assignments while preserving magic trailing commas and intentional multiline formatting.
Input
(c, *_) = a()
Output (off) unchanged
Output (on)
c, *_ = a()
An unobjective change, but can cause lots of churn, and was added very recently (Not released yet, will hopefully be in a 25.12).
fix_type_expansion_split (#4777)
Fix type expansions split in generic functions.
Input
def func1[T: (int, str)](a,): ...
Output (off)
def func1[
T: (int, str)
](a,): ...
Output (on)
def func1[T: (int, str)](
a,
): ...
Causes a good amount of changes, but should be fairly objective.
standardize_type_comments (#4645)
Format type comments which have zero or more spaces between # and type: or between type: and value to # type: (value).
Input
# type: ignore
Output (off) unchanged
Output (on)
# type: ignore
Causes changes, but they are minimal, localized, and objective.
always_one_newline_after_import (#4489)
Always force one blank line after import statements, except when the line after the import is a comment or an import statement.
Input
from middleman.authentication import validate_oauth_token
logger = logging.getLogger(__name__)
Output (off) unchanged
Output (on)
from middleman.authentication import validate_oauth_token
logger = logging.getLogger(__name__)
Only changes one part of each file, and changes are minimal and objective. Was deferred in 2025, just because it was new.
remove_parens_around_except_types (#4720)
Remove parentheses around multiple exception types in except and except* without as. See PEP 758 for details.
Input
try:
...
except (A, B, C):
...
Output (off) unchanged
Output (on)
try:
...
except A, B, C:
...
New feature introduced in & gated to Python 3.14. No changes in most codebases, which are under 3.14. When there are changes, they're minimal, localized, and objective.
fix_module_docstring_detection (#4764)
Fix module docstrings being treated as normal strings if preceded by comments.
Input
# comment
"""
docstring
"""
from __future__ import annotations
Output (off) unchanged
Output (on)
# comment
"""
docstring
"""
from __future__ import annotations
Bug fix; only changes one thing per file at max.
normalize_cr_newlines (#4710)
Add \r style newlines to the potential newlines to normalize file newlines both from and to.
Input
a[CR]
b[LF]
c[CR][LF]
#1[CR][LF]
Output (off)
a[CR]
b[LF]
c[CR]
# 1[CR]
Output (on)
a[CR]
b[CR]
c[CR]
# 1[CR]
Bug fix, was only ever found by Fuzz and shouldn't cause any noticable source code changes in practice.
fix_fmt_skip_in_one_liners (#4800)
Fix # fmt: skip behavior on one-liner declarations, such as def foo(): return "mock" # fmt: skip, where previously the declaration would have been incorrectly collapsed.
Input
if True: print("this"); print("that") # fmt: skip
Output (off)
if True:
print("this"); print("that") # fmt: skip
Output (on) unchanged
Purely a bug fix, shouldn't actually change any pre-formatted code.
Also, we should remember to regenerate _width_table.py for v26 (#4253).
IMO, everything fix_type_expansion_split and below should definitely be stabilized. I see arguments either way for the items above it. Discussion welcome!
I don't use python very often, but when I do, I use black. Coming from a rust background, the more "controversial" features seem like the sensible thing to me. Vertical code is (IMO) usually easier to reason about than horizontal code, and python can get very horizontal.
I'm less sure about standardize_type_comments - changing comments seems strange to me. But, if there are other rules that alter comments, there is a reasonable precedent for this.
Agree that everything fix_type_expansion_split and below should be stabilised!
I ran black --preview on a sizable fraction of a giant codebase.
-
multiline_string_handlinglooks great, we should stabilise it -
wrap_long_dict_values_in_parensresults in a tonne of change. A lot of it actually isn't great, I kind of don't want it. A lot of putting a really long string on its own line, which just eats more vertical space, and makes the dict keys harder to scan. -
wrap_comprehension_inis mostly great, but in a few cases removes parentheses that makes things harder to read. I opened https://github.com/psf/black/issues/4877 , ideally we would fix this
I didn't test remove_parens_from_assignment_lhs because I was using 25.11
wrap_long_dict_values_in_parensresults in a tonne of change. A lot of it actually isn't great, I kind of don't want it. A lot of putting a really long string on its own line, which just eats more vertical space, and makes the dict keys harder to scan.
@hauntsaninja Are you able to share examples?
Related: #4158 which asked for the opposite (more wrapping) and was implemented last year
This feature has been around for over 2 years so we should probably try to reach a decision on it soon
Good spot on #4158. I don't know that this is my most principled opinion, but posted some examples in https://github.com/psf/black/issues/4882 . Curious what folks' takes here are, or if other people have opinions on wrap_long_dict_values_in_parens from trying it out on their codebases!
An unobjective change, but can cause lots of churn, and was added very recently (Not released yet, will hopefully be in a 25.12).
Ideally every change we make should be in the preview style at least once, so would be good to get this in a 25.12. I get the impression that this particular change won't touch a lot of code, so probably not a big deal.
I tried black --preview on a couple of open-source codebases (my work codebase is much bigger but Shantanu already covered it). Mostly I see few changes and I like the changes that I see; most seem to be from multiline_string_handling.
In mypy I'm not sure we should be removing these parens:
diff --git a/mypy/graph_utils.py b/mypy/graph_utils.py
index 154efcef4..a58c58bf9 100644
--- a/mypy/graph_utils.py
+++ b/mypy/graph_utils.py
@@ -113,5 +113,5 @@ def topsort(data: dict[T, set[T]]) -> Iterable[set[T]]:
if not ready:
break
yield ready
- data = {item: (dep - ready) for item, dep in data.items() if item not in ready}
+ data = {item: dep - ready for item, dep in data.items() if item not in ready}
assert not data, f"A cyclic dependency exists amongst {data!r}"
This is another example of wrap_long_dict_values_in_parens. I think I'm fine with the reformatting here but there may be cases where it's worse.
Also a case where removing parens from a comprehension is clearly a positive change:
diff --git a/mypy/test/helpers.py b/mypy/test/helpers.py
index d5f1cd787..7e7db1b32 100644
--- a/mypy/test/helpers.py
+++ b/mypy/test/helpers.py
@@ -481,7 +481,7 @@ def normalize_report_meta(content: list[str]) -> list[str]:
def find_test_files(pattern: str, exclude: list[str] | None = None) -> list[str]:
return [
path.name
- for path in (pathlib.Path(test_data_prefix).rglob(pattern))
+ for path in pathlib.Path(test_data_prefix).rglob(pattern)
if path.name not in (exclude or [])
]
So overall I'm positive on these changes, though we should consider tweaking or deferring the parens-related ones.
The changes for Pillow with Black 26.1a1 look good to me 👍 https://github.com/hugovk/Pillow/commit/485ed4ec3a629ceccdfe13479c6349e13a3bdc28
Just tested the preview style for pylint (with 25.12.0).
wrap_long_dict_values_in_parens
Not entirely sure about this one, especially for strings. There are some good examples, e.g.
{
"type": "csv",
"metavar": "<modules>",
"default": (),
- "help": "List of plugins (as comma separated values of "
- "python module names) to load, usually to register "
- "additional checkers.",
+ "help": (
+ "List of plugins (as comma separated values of "
+ "python module names) to load, usually to register "
+ "additional checkers."
+ ),
},
However for single line ones it's arguably worse. Especially if it's just a few chars over the line length limit. I'd definitely add # fmt: off in a few places because of it.
{
"default": False,
"type": "yn",
"metavar": "<y or n>",
- "help": "Include a hint for the correct naming format with invalid-name.",
+ "help": (
+ "Include a hint for the correct naming format with invalid-name."
+ ),
},
multiline_string_handling
I like it. Yes it causes a bit of churn but it always looked kind of strange to have this on separate lines.
"""
)
wrap_comprehension_in
We do have a similar example to #4877 where the parentheses were added for additional clarity, removing them feels wrong.
- kwargs = {keyword.arg for keyword in (node.keywords or ())}
+ kwargs = {keyword.arg for keyword in node.keywords or ()}
For multiline examples, yes it is consistent but I always felt like wrapping something without any operators just so the line length is kept below the limit, isn't great.
any(
other_node_final_statement is closest_try_finally_ancestor
or other_node_final_statement.parent_of(
closest_try_finally_ancestor
)
- for other_node_final_statement in other_node_try_finally_ancestor.finalbody
+ for other_node_final_statement in (
+ other_node_try_finally_ancestor.finalbody
+ )
)
If it were up to me, I'd also prefer closest_try_finally_ancestor to be on the same line with other_node_final_statement.parent_of (though not related to this particular change).
Thanks for the feedback! FWIW @cdce8p, we're expecting to defer wrap_long_dict_values_in_parens and wrap_comprehension_in to next year (hence they're not included in 26.1a1). The insights are still appreciated though!