Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
74 changes: 30 additions & 44 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
# Change Log

## Unreleased

<!-- PR authors:
Please include the PR number in the changelog entry, not the issue number -->
## 26.1.0

### Highlights

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

This release alo bumps `pathspec` to v1.0.0 and fixes inconsistencies with Git's
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 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
format `exclude/not_this/foo.py` with this `.gitignore`:
Expand All @@ -31,49 +53,14 @@ This new behavior matches Git. The leading `*/` are only necessary if you wish t
matching subdirectories (like the previous behavior did), and not just matching root
directories.

### Stable style

<!-- Changes that affect Black's stable style -->

### Preview style

<!-- Changes that affect Black's preview style -->

### Configuration

<!-- Changes to how Black can be configured -->

### Packaging

<!-- Changes to how Black is packaged, such as dependency requirements -->

### Parser

<!-- Changes to the parser or to version autodetection -->

### Performance

<!-- Changes that improve Black's performance. -->

### Output

<!-- Changes to Black's terminal output and error messages -->

### _Blackd_

<!-- Changes to blackd -->
- Explicitly shutdown the multiprocessing manager when run in diff mode too (#4952)

### Integrations

<!-- For example, Docker, GitHub Actions, pre-commit, editors -->

- Upgraded PyPI upload workflow to use Trusted Publishing (#4611)

### Documentation

<!-- Major changes to documentation and policies. Small docs changes
don't need a changelog entry. -->

## 25.12.0

### Highlights
Expand All @@ -82,7 +69,6 @@ directories.

### Stable style

- Fix Shutdown multiprocessing Manager in schedule_formatting (#4952)
- Fix bug where comments preceding `# fmt: off`/`# fmt: on` blocks were incorrectly
removed, particularly affecting Jupytext's `# %% [markdown]` comments (#4845)
- Fix crash when multiple `# fmt: skip` comments are used in a multi-part if-clause, on
Expand Down
6 changes: 3 additions & 3 deletions docs/integrations/source_version_control.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Use [pre-commit](https://pre-commit.com/). Once you
repos:
# Using this mirror lets us use mypyc-compiled black, which is about 2x faster
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.12.0
rev: 26.1.0
hooks:
- id: black
# It is recommended to specify the latest version of Python
Expand All @@ -35,7 +35,7 @@ include Jupyter Notebooks. To use this hook, simply replace the hook's `id: blac
repos:
# Using this mirror lets us use mypyc-compiled black, which is about 2x faster
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.12.0
rev: 26.1.0
hooks:
- id: black-jupyter
# It is recommended to specify the latest version of Python
Expand Down Expand Up @@ -66,7 +66,7 @@ Configure exclusions directly in `.pre-commit-config.yaml`:
```yaml
repos:
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.12.0
rev: 26.1.0
hooks:
- id: black
exclude: ^migrations/|^generated/
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
6 changes: 3 additions & 3 deletions docs/usage_and_configuration/the_basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,8 @@ configuration file for consistent results across environments.

```console
$ black --version
black, 25.12.0 (compiled: yes)
$ black --required-version 25.12.0 -c "format = 'this'"
black, 26.1.0 (compiled: yes)
$ black --required-version 26.1.0 -c "format = 'this'"
format = "this"
$ black --required-version 31.5b2 -c "still = 'beta?!'"
Oh no! 💥 💔 💥 The required version does not match the running version!
Expand Down Expand Up @@ -380,7 +380,7 @@ You can check the version of _Black_ you have installed using the `--version` fl

```console
$ black --version
black, 25.12.0
black, 26.1.0
```

#### `--config`
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ diff-shades = [
]
diff-shades-comment = ["click>=8.1.7", "packaging>=22.0", "urllib3"]

width-table = ["wcwidth>=0.2.6"]
width-table = ["wcwidth>=0.2.14"]

[project.scripts]
black = "black:patched_main"
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