Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
96a5903
Sort lists
cobaltt7 Dec 7, 2025
581ad21
always_one_newline_after_import
cobaltt7 Dec 7, 2025
f833cea
fix_fmt_skip_in_one_liners
cobaltt7 Dec 7, 2025
b973b53
fix_module_docstring_detection
cobaltt7 Dec 7, 2025
c8f2baa
fix_type_expansion_split
cobaltt7 Dec 7, 2025
561f2ce
multiline_string_handling
cobaltt7 Dec 7, 2025
4f4d8ce
normalize_cr_newlines
cobaltt7 Dec 7, 2025
b195bc1
remove_parens_around_except_types
cobaltt7 Dec 7, 2025
e3927b5
remove_parens_from_assignment_lhs
cobaltt7 Dec 7, 2025
55b902c
standardize_type_comments
cobaltt7 Dec 7, 2025
e8b6091
Update changelog
cobaltt7 Dec 7, 2025
0e2409f
Regenerate `_width_table.py` & add tests for the Khmer language (#4253)
KaiSforza Dec 7, 2025
b431e55
Update most tests
cobaltt7 Dec 7, 2025
2fd8bc6
Remove unused imports
cobaltt7 Dec 7, 2025
36378d1
Update changelog
cobaltt7 Dec 7, 2025
275834d
Fix newlines being added after imports with `# fmt: skip` on them
cobaltt7 Dec 8, 2025
70c9792
Merge branch 'main' of https://github.com/psf/black into black26
cobaltt7 Dec 8, 2025
e8e64f1
Remove newline on fmt:off line after import
cobaltt7 Dec 8, 2025
dfd5408
Merge branch 'main' of https://github.com/psf/black into black26
cobaltt7 Dec 8, 2025
3e305dd
update changelog
cobaltt7 Dec 19, 2025
a3df868
Merge main into black26
cobaltt7 Jan 17, 2026
8ea125e
Bump wcwidth version in pyproject.toml to avoid downgrading the used …
cobaltt7 Jan 17, 2026
4b82117
Update flags in tests
cobaltt7 Jan 18, 2026
b4865ff
change pathspec version in changelog
cobaltt7 Jan 18, 2026
ca22cee
Run release.py
cobaltt7 Jan 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,33 @@

<!-- Include any especially major or disruptive changes here -->

Introduces the 2026 stable style (#4892), stabilizing the following changes:

- `always_one_newline_after_import`: Always force one blank line after import
statements, except when the line after the import is a comment or an import statement
(#4489)
- `fix_fmt_skip_in_one_liners`: 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 (#4800)
- `fix_module_docstring_detection`: Fix module docstrings being treated as normal
strings if preceded by comments (#4764)
- `fix_type_expansion_split`: Fix type expansions split in generic functions (#4777)
- `multiline_string_handling`: Make expressions involving multiline strings more compact
(#1879)
- `normalize_cr_newlines`: Add `\r` style newlines to the potential newlines to
normalize file newlines both from and to (#4710)
- `remove_parens_around_except_types`: Remove parentheses around multiple exception
types in `except` and `except*` without `as` (#4720)
- `remove_parens_from_assignment_lhs`: Remove unnecessary parentheses from the left-hand
side of assignments while preserving magic trailing commas and intentional multiline
formatting (#4865)
- `standardize_type_comments`: Format type comments which have zero or more spaces
between `#` and `type:` or between `type:` and value to `# type: (value)` (#4645)

The following change was not in any previous stable release:

- Regenerated the `_width_table.py` and added tests for the Khmer language (#4253)

This release alo bumps `pathspec` to v1.0.0 and fixes inconsistencies with Git's
`.gitignore` logic (#4958). Now, files will be ignored if a pattern matches them, even
if the parent directory is directly unignored. For example, Black would previously
Expand Down
83 changes: 5 additions & 78 deletions docs/the_black_code_style/future_style.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,10 @@ experimental, feedback and issue reports are highly encouraged!

Currently, the following features are included in the preview style:

- `always_one_newline_after_import`: Always force one blank line after import
statements, except when the line after the import is a comment or an import statement
- `wrap_long_dict_values_in_parens`: Add parentheses around long values in dictionaries
([see below](labels/wrap-long-dict-values))
- `fix_fmt_skip_in_one_liners`: Fix `# fmt: skip` behaviour on one-liner declarations,
such as `def foo(): return "mock" # fmt: skip`, where previously the declaration would
have been incorrectly collapsed.
- `standardize_type_comments`: Format type comments which have zero or more spaces
between `#` and `type:` or between `type:` and value to `# type: (value)`
- `wrap_comprehension_in`: Wrap the `in` clause of list and dictionary comprehensions
across lines if it would otherwise exceed the maximum line length.
- `remove_parens_around_except_types`: Remove parentheses around multiple exception
types in `except` and `except*` without `as`. See PEP 758 for details.
- `normalize_cr_newlines`: Add `\r` style newlines to the potential newlines to
normalize file newlines both from and to.
- `fix_module_docstring_detection`: Fix module docstrings being treated as normal
strings if preceeded by comments.
- `fix_type_expansion_split`: Fix type expansions split in generic functions.
- `remove_parens_from_assignment_lhs`: Remove unnecessary parentheses from the left-hand
side of assignments while preserving magic trailing commas and intentional multiline
formatting. For example, `(b) = a()[0]` becomes `b = a()[0]`, and `(c, *_) = a()`
becomes `c, *_ = a()`, but `(d,) = a()` is preserved as it defines a single-element
tuple.
- `multiline_string_handling`: more compact formatting of expressions involving
multiline strings ([see below](labels/multiline-string-handling))
- `fix_module_docstring_detection`: Fix module docstrings being treated as normal
strings if preceeded by comments.
- `wrap_long_dict_values_in_parens`: Add parentheses around long values in dictionaries.
([see below](labels/wrap-long-dict-values))

(labels/wrap-long-dict-values)=

Expand Down Expand Up @@ -68,56 +45,6 @@ my_dict = {
}
```

(labels/multiline-string-handling)=

### Improved multiline string handling

_Black_ is smarter when formatting multiline strings, especially in function arguments,
to avoid introducing extra line breaks. Previously, it would always consider multiline
strings as not fitting on a single line. With this new feature, _Black_ looks at the
context around the multiline string to decide if it should be inlined or split to a
separate line. For example, when a multiline string is passed to a function, _Black_
will only split the multiline string if a line is too long or if multiple arguments are
being passed.

For example, _Black_ will reformat

```python
textwrap.dedent(
"""\
This is a
multiline string
"""
)
```

to:

```python
textwrap.dedent("""\
This is a
multiline string
""")
```

And:

```python
MULTILINE = """
foobar
""".replace(
"\n", ""
)
```

to:

```python
MULTILINE = """
foobar
""".replace("\n", "")
```

## Unstable style

(labels/unstable-style)=
Expand All @@ -135,9 +62,9 @@ demoted from the `--preview` to the `--unstable` style, users can use the

The unstable style additionally includes the following features:

- `hug_parens_with_braces_and_square_brackets`: more compact formatting of nested
brackets ([see below](labels/hug-parens))
- `string_processing`: split long string literals and related changes
- `hug_parens_with_braces_and_square_brackets`: More compact formatting of nested
brackets. ([see below](labels/hug-parens))
- `string_processing`: Split long string literals and related changes.
([see below](labels/string-processing))

(labels/hug-parens)=
Expand Down
5 changes: 0 additions & 5 deletions scripts/fuzz.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,6 @@
def test_idempotent_any_syntatically_valid_python(
src_contents: str, mode: black.FileMode
) -> None:
if (
"#\r" in src_contents or "\\\n" in src_contents
) and black.Preview.normalize_cr_newlines not in mode:
return

# Before starting, let's confirm that the input string is valid Python:
compile(src_contents, "<string>", "exec") # else the bug is in hypothesmith

Expand Down
75 changes: 25 additions & 50 deletions src/black/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1010,10 +1010,8 @@ def format_stdin_to_stdout(

if content is None:
src, encoding, newline = decode_bytes(sys.stdin.buffer.read(), mode)
elif Preview.normalize_cr_newlines in mode:
src, encoding, newline = content, "utf-8", "\n"
else:
src, encoding, newline = content, "utf-8", ""
src, encoding, newline = content, "utf-8", "\n"

dst = src
try:
Expand All @@ -1029,12 +1027,8 @@ def format_stdin_to_stdout(
)
if write_back == WriteBack.YES:
# Make sure there's a newline after the content
if Preview.normalize_cr_newlines in mode:
if dst and dst[-1] != "\n" and dst[-1] != "\r":
dst += newline
else:
if dst and dst[-1] != "\n":
dst += "\n"
if dst and dst[-1] != "\n" and dst[-1] != "\r":
dst += newline
f.write(dst)
elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
now = datetime.now(timezone.utc)
Expand Down Expand Up @@ -1224,16 +1218,13 @@ def f(
def _format_str_once(
src_contents: str, *, mode: Mode, lines: Collection[tuple[int, int]] = ()
) -> str:
if Preview.normalize_cr_newlines in mode:
normalized_contents, _, newline_type = decode_bytes(
src_contents.encode("utf-8"), mode
)
normalized_contents, _, newline_type = decode_bytes(
src_contents.encode("utf-8"), mode
)

src_node = lib2to3_parse(
normalized_contents.lstrip(), target_versions=mode.target_versions
)
else:
src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions)
src_node = lib2to3_parse(
normalized_contents.lstrip(), target_versions=mode.target_versions
)

dst_blocks: list[LinesBlock] = []
if mode.target_versions:
Expand Down Expand Up @@ -1280,22 +1271,9 @@ def _format_str_once(
for block in dst_blocks:
dst_contents.extend(block.all_lines())
if not dst_contents:
if Preview.normalize_cr_newlines in mode:
if "\n" in normalized_contents:
return newline_type
else:
# Use decode_bytes to retrieve the correct source newline (CRLF or LF),
# and check if normalized_content has more than one line
normalized_content, _, newline = decode_bytes(
src_contents.encode("utf-8"), mode
)
if "\n" in normalized_content:
return newline
return ""
if Preview.normalize_cr_newlines in mode:
return "".join(dst_contents).replace("\n", newline_type)
else:
return "".join(dst_contents)
if "\n" in normalized_contents:
return newline_type
return "".join(dst_contents).replace("\n", newline_type)


def decode_bytes(src: bytes, mode: Mode) -> tuple[FileContent, Encoding, NewLine]:
Expand All @@ -1309,24 +1287,21 @@ def decode_bytes(src: bytes, mode: Mode) -> tuple[FileContent, Encoding, NewLine
if not lines:
return "", encoding, "\n"

if Preview.normalize_cr_newlines in mode:
if lines[0][-2:] == b"\r\n":
if b"\r" in lines[0][:-2]:
newline = "\r"
else:
newline = "\r\n"
elif lines[0][-1:] == b"\n":
if b"\r" in lines[0][:-1]:
newline = "\r"
else:
newline = "\n"
if lines[0][-2:] == b"\r\n":
if b"\r" in lines[0][:-2]:
newline = "\r"
else:
if b"\r" in lines[0]:
newline = "\r"
else:
newline = "\n"
newline = "\r\n"
elif lines[0][-1:] == b"\n":
if b"\r" in lines[0][:-1]:
newline = "\r"
else:
newline = "\n"
else:
newline = "\r\n" if lines[0][-2:] == b"\r\n" else "\n"
if b"\r" in lines[0]:
newline = "\r"
else:
newline = "\n"

srcbuf.seek(0)
with io.TextIOWrapper(srcbuf, encoding) as tiow:
Expand Down
Loading
Loading