Today we released Black 23.1.0 🎉.

While Black does not follow SemVar, it is indeed a major release as it ships with the 2023 stable style. Many changes made to the preview style over the past year have been finally promoted to the stable style.

You can install it by running this command:

python -m pip install black==23.1.0

Below I’ll cover the important changes worth mentioning. If you’d like the full changelog for 23.1.0, please refer to the link above.

Code style

The 2023 stable style

We didn’t make any major changes to the 2023 stable style draft since 23.1a1.

So for the major parts of the new stable style, please refer to the 23.1a1 post. Everything under the “promoted changes” header were promoted officially in 23.1.0. The TL;DR is:

  • Improved empty line handling (mostly removing unnecessary ones)
  • Removal of redundant parentheses in several contexts

We did also fix quite a few bugs and crashes, including these ones reporting by users testing out 23.1a1:

For the rest, please refer to the changelog.

The 2023 preview style

Conditional expressions are parenthesized if needed now

Issue: #2248 ~ PR: #2278

If a conditional expression is too long, it will be wrapped in parentheses instead of whatever nonsense Black did before.

I’ll just let this diff speak for itself :)

--- pyanalyze/	2021-05-30 03:06:13.755715 +0000
+++ pyanalyze/	2021-05-30 03:06:32.534930 +0000
@@ -448,13 +448,13 @@
                 arg = arg.replace(kind=SigParameter.POSITIONAL_OR_KEYWORD)
         return Signature.make(
-            return_annotation=GenericValue(Awaitable, [return_value])
-            if is_async_fn
-            else return_value,
+            return_annotation=(
+                GenericValue(Awaitable, [return_value]) if is_async_fn else return_value
+            ),

     def _parse_param_list(
         args: Iterable[ast3.arg],

There are certain cases where it might be overkill, like this one, but overall, I consider it a major improvement.

             normalized = [
-                (source, source)
-                if source == "-"
-                else (normalize_path_maybe_ignore(Path(source), root), source)
+                (
+                    (source, source)
+                    if source == "-"
+                    else (normalize_path_maybe_ignore(Path(source), root), source)
+                )
                 for source in src

Wrap multiple context managers in parentheses if targeting Python 3.9+

Issues: #3486 + #664 ~ PR: #3489

Since Python 3.91, you can break long with statements using parentheses. Thanks to work done by @yilei, Black will now use this style as long as the lowest targeted version is 3.9.


with make_context_manager1() as cm1, make_context_manager2() as cm2, make_context_manager3() as cm3, make_context_manager4() as cm4:

Before: unchanged!!


with (
    make_context_manager1() as cm1,
    make_context_manager2() as cm2,
    make_context_manager3() as cm3,
    make_context_manager4() as cm4,

Black still doesn’t have a good story for formatting such with statements when Python <3.9 support is requested. All of the gory details can be found in issue #664. (The summary is that no one has stepped up to implement the proposed style.)


mypyc wheels are available for all platforms (once again)

Long story short, recent versions of packaging and hatchling interacted with ways that led to errors when trying to build macOS mypyc wheels. The essence is that the wrong platform tag was being chosen which made the wheels uninstallable in certain situations (including on the same machine that built wheels >.<).

Anyway that’s all fixed now and 23.1.0 ships with macOS wheels, bringing us back to the full set of compiled wheels.

This affected the 22.10.0, 22.12.0, and 23.1a1 releases.

mypycified Black can be now built on armv7

We upgraded mypy/c from 0.971 which 0.991 which allows mypycified Black to be built on armv7.

Conveniently, this also fixes some segfaults caused by mypyc:


Issue: #3384 ~ PR: #3385

When trying to format a project from the outside, the verbose output shows says that there are symbolic links that points outside of the project, but displays the wrong project path. This behavior was triggered when Black is executed from outside the project’s root.

Consider the following tree:

└── home/
    └── project/
        ├── .git
        └── dir/

When trying to format a folder from home, this is the output:

$ black ./project --check --verbose
Identified `/path/to/home/project` as project root containing a .git directory.
Sources to be formatted: "."
project/.git ignored: is a symbolic link that points outside /path/to/home/project/project
project/.git ignored: matches the --exclude regular expression
project/dir ignored: is a symbolic link that points outside /path/to/home/project/project
project/dir/ ignored: is a symbolic link that points outside /path/to/home/project/project
would reformat project/dir/

Oh no! 💥 💔 💥
1 file would be reformatted.

Notice that the message $FILEPATH ignored: is a symbolic link that points outside $PROJECTPATH displays a wrong $PROJECTPATH (should be /path/to/home/project/ instead of /path/to/home/project/project).

Anyway, Black will no longer emit these superfluous and incorrect messages. All thanks goes to @aaossa.

Verbose output now shows the full loaded configuration

Issue: #3386 ~ PR: #3392

It’s easier to just copy and paste part of the issue to explain this feature:

Sometimes when dealing with multiple environment settings, IDEs, and pyproject.toml files etc. etc. is beneficial for a project team to have a confirmation about the actual applied black-settings (“line length”) in the CI CD pipeline.

black -v will show a line like this: Using configuration from project root.. However, it doesn’t tell us what the settings are […]

… so we changed Black to show the loaded configuration if -v / --verbose is passed.

$ black src/black/ --verbose
Identified `/home/ichard26/programming/oss/black` as project root containing a .git directory.
Sources to be formatted: "src/black/"
Using configuration from project root.
line_length: 88
target_version: ['py37', 'py38']
include: \.pyi?$
extend_exclude: /(
  # The following are specific to Black, you probably don't want those.
  | blib2to3
  | tests/data
  | profiling

preview: True
src/black/ already well formatted, good job.

All done! ✨ 🍰 ✨
1 file left unchanged.

Some cleaning up can definitely be done to the verbose output, but at least it’s there now.


Better default for –target-version if PEP 621 python-requires is available

Issue: #3124 ~ PR: #3219

Black will now infer a default set of target versions using the project.requires-python field in pyproject.toml if possible.

For example, these are the inferred default for --target-version for three different requires-python values:

  • 3.8.5 becomes ["py38"]
  • >3.6,<3.11 becomes ["py37", "py38", "py39", "py310"]
  • <3.7 becomes ["py33", "py34", "py35", "py36"]
    • Python 3.3 is the minimal supported version for Black

If you’re curious what Black infers for your project, make sure to comment out any --target-version configuration you have set and run Black with --verbose. Thanks to the change mentioned previously, there should be a line showing the (inferred) target-version config value.

If --target-version is explicitly configured on the command line or in pyproject.toml, it will trump the inferred configuration. And if Black encounters any error inferring a better default, it will simply fallback to per-file autodetection (like it did before).

To make this work, Black now requires packaging (version 22 and up). It’s a tiny dependency and often installed with other development tools so it shouldn’t be a big deal.

In my opinion, the general hope is that the vast majority of projects will eventually use Black’s inference capabilities instead of setting --target-version separately. It’s less work (no need to update it!) and should be less error prone.

At the bare minimum, new projects shouldn’t have to configure --target-version anymore as long as they define their project metadata in the standardized way.

This was contributed by the lovely @stinodego.

A few final words

We tried to do some community outreach in an attempt to iron out issues before cutting 23.1.0 and make sure the 2023 stable style wouldn’t be a failure. We got some helpful feedback, but I realize we could’ve done more.

If you have any feedback for this release or suggestions for next time, please let us know! Feel free to open an issue, email me, message us on Discord, whatever.

Anyway, thanks for reading this. On behalf of the maintainer team, I hope you enjoy the new release!

  1. Well officially speaking, this is only supported starting in Python 3.10 and higher, but GvR and co. might have snuck it into 3.9 :P ↩︎