As the post summary says, earlier today we released Black 23.1a1. You might be wondering why we released an alpha… it’s because the upcoming Black 23.1.0 release will see many preview code style changes promoted to the stable style.

In other words, Black 23.1.0 will format your code differently out of the box.

If you’re surprised we’re doing this, our stability policy allows us to update the stable style in the new year. This minimises disruption while keeping the door for improvements open.

23.1a1 contains a draft of the 2023 stable style that we, the maintainers, initially decided on.

However, we need your help in finalizing what goes into the 2023 stable style. If you can, please try 23.1a1 out on your codebase(s). You can install it by running this command:

python -m pip install black==23.1a1

If you have any feedback, concerns, or run into any issues, please drop us a comment in this issue (https://github.com/psf/black/issues/3407).

Note: Please read the start of the issue description before commenting. It will make the lives of everyone easier.

What follows is brief rundown of all of the changes in the preview style that were promoted to stable in 23.1a1.

Bugfixes

--skip-string-normalization now prevents docstring prefixes from being normalized

-S / --skip-string-normalization stops Black from normalizing string prefixes and quotes, except when it doesn’t…

Sometime ago I accidentally added a bug where Black would normalize docstring prefixes even if you told it not to, leading to this:

 def add(a, b):
-    F"""Add two numbers and return the result."""
+    f"""Add two numbers and return the result."""
     return a + b

This has been fixed in the preview style since 22.8.0 (leaving this example unchanged).

--skip-magic-trailing-comma now removes useless trailing commas in subscripts

Black has the concept of “magic trailing commas.” You can read about it here, but in essence, Black treats trailing commas as a sign that collections, calls, or function signatures should always be exploded.

# The trailing comma will stop Black from collapsing this collection into one line.
TRANSLATIONS = {
    "en_us": "English (US)",
    "pl_pl": "polski",
}

This can be disabled passing the -C / --skip-magic-trailing-comma flag.1

However, Black didn’t remove unnecessary trailing commas in all situations. In the following example, the trailing comma in the function signature was removed as expected, but not the subscript.

 Point = Tuple[int, int,]

-def f(a: Point, b: Point,): ...
+def f(a: Point, b: Point): ...

This was fixed in the preview style since 22.8.0, which removes both commas as expected.

-Point = Tuple[int, int,]
+Point = Tuple[int, int]

-def f(a: Point, b: Point,): ...
+def f(a: Point, b: Point): ...

Correctly handle trailing commas that are inside a line’s leading non-nested parens

There are actually two problems in the “before” output:

  • The zero(one,).two(three,).four(five,) chain should be fully exploded
  • The magic trailing comma in the dictionary in refresh_token() is being ignored

Source:

zero(one,).two(three,).four(five,)


def refresh_token(self, device_family, refresh_token, api_key):
    return self.get(
        data={
            "refreshToken": refresh_token,
        },
        api_key=api_key,
    )["sdk"]["token"]

Before:

zero(one,).two(three,).four(
    five,
)


def refresh_token(self, device_family, refresh_token, api_key):
    return self.get(data={"refreshToken": refresh_token,}, api_key=api_key,)[
        "sdk"
    ]["token"]

After:

zero(
    one,
).two(
    three,
).four(
    five,
)


def refresh_token(self, device_family, refresh_token, api_key):
    return self.get(
        data={
            "refreshToken": refresh_token,
        },
        api_key=api_key,
    )[
        "sdk"
    ]["token"]

Both these issues have been fixed in the preview style since 22.12.0.

End quotes that violate the line length limit in multiline docstrings will be moved

1
2
3
4
5
6
7
8
9
def process_and_count(text):
    """
    Remove punctuation and return cleaned string, in addition to its length in tokens."""
    pass


def example_2(text):
    """Remove punctuation and return cleaned string in addition to its length in tokens."""
    pass

Here, the third line violates the line length limit (90 > 88) because of the end quotes. Earlier versions of Black would leave this be which wasn’t ideal, so now they are moved onto their own line.

 def process_and_count(text):
     """
-    Remove punctuation and return cleaned string, in addition to its length in tokens."""
+    Remove punctuation and return cleaned string, in addition to its length in tokens.
+    """
     pass

However, Black won’t move quotes of single line docstrings (such as with example_2()) since that would look ugly.

This was added to the preview style in 22.8.0 and then tweaked in 23.1a1.

Better management of parentheses

Here’s a set of changes that together improve Black’s handling of parentheses in various situations.

Out of these five, “Remove redundant (outermost) parentheses in for statements” will probably be the most impactful.

Return type annotations

Source:

def foo() -> (int):
    return 2


def foo() -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds:
    return 2


def frobnicate() -> ThisIsTrulyUnreasonablyExtremelyLongClassName | list[ThisIsTrulyUnreasonablyExtremelyLongClassName]:
    pass

Before:

def foo() -> (int):
    return 2


def foo() -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds:
    return 2


def frobnicate() -> ThisIsTrulyUnreasonablyExtremelyLongClassName | list[
    ThisIsTrulyUnreasonablyExtremelyLongClassName
]:
    pass

After (22.6.0+):

def foo() -> int:
    return 2


def foo() -> (
    intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds
):
    return 2


def frobnicate() -> (
    ThisIsTrulyUnreasonablyExtremelyLongClassName
    | list[ThisIsTrulyUnreasonablyExtremelyLongClassName]
):
    pass

Remove redundant parentheses around awaited objects

Source:

async def main():
    await (asyncio.sleep(1))
    await (set_of_tasks | other_set)

Before: unchanged.

After (22.6.0+):

async def main():
    await asyncio.sleep(1)
    await (set_of_tasks | other_set)

Remove redundant parentheses in with statements

Source:

with (open("bla.txt")):
    pass

with (open("bla.txt")), (open("bla.txt")):
    pass

with (open("bla.txt") as f):
    pass

with (open("bla.txt")) as f:
    pass

Before: unchanged.

After (22.6.0+):

with open("bla.txt"):
    pass

with open("bla.txt"), open("bla.txt"):
    pass

with open("bla.txt") as f:
    pass

with open("bla.txt") as f:
    pass

Remove redundant parentheses from except clauses

Source:

try:
    a.something
except (AttributeError) as err:
    raise err

Before: unchanged.

After (22.3.0+):

try:
    a.something
except AttributeError as err:
    raise err

Remove redundant (outermost) parentheses in for statements

Source:

for (k, v) in d.items():
    print(k, v)

Before: unchanged.

After (22.3.0+):

for k, v in d.items():
    print(k, v)

Unfortunately, nested parentheses are still left untouched. I have an open PR to fix that, but it isn’t ready yet.

Remove blank lines after code block open

Here’s another change that is likely to impact your codebase.

Note: This new feature will be applied to all code blocks: def, class, if, for , while, with, case and match.

Source:

def foo():



    print("All the newlines above me should be deleted!")


if condition:

    print("No newline above me!")

    print("There is a newline above me, and that's OK!")


class Point:

    x: int
    y: int

    def as_tuple(self) -> tuple[int, int]:
        return (self.x, self.y)

Before:

def foo():

    print("All the newlines above me should be deleted!")


if condition:

    print("No newline above me!")

    print("There is a newline above me, and that's OK!")


class Point:

    x: int
    y: int

    def as_tuple(self) -> tuple[int, int]:
        return (self.x, self.y)

After (22.6.0+):

def foo():
    print("All the newlines above me should be deleted!")


if condition:
    print("No newline above me!")

    print("There is a newline above me, and that's OK!")


class Point:
    x: int
    y: int

    def as_tuple(self) -> tuple[int, int]:
        return (self.x, self.y)

Ignore magic trailing comma for single-element subscripts

Black exempts single-element tuple literals from the usual handling of magic trailing commas: (1,) will not have a newline added (as would happen for lists, sets, etc.).

However, if you wrote tuple[int,] (to make it more visually distinctive to list[int]), Black would explode it, which doesn’t look great.

Source:

shape: tuple[int,]
shape = (1,)

Before:

shape: tuple[
    int,
]
shape = (1,)

After (22.3.0+): unchanged.

Enforce empty lines before classes/functions with sticky leading comments

The old (“before”) behaviour here caused flake8 to raise E302.

Source:

some_var = 1
# Leading sticky comment
def some_func():
    ...


def get_search_results(self, request, queryset, search_term):
    """
    Return a tuple containing a queryset to implement the search
    and a boolean indicating if the results may contain duplicates.
    """
    # Apply keyword searches.
    def construct_search(field_name):
        ...

    ...

Before: unchanged.

After (22.12.0+):

some_var = 1


# Leading sticky comment
def some_func():
    ...


def get_search_results(self, request, queryset, search_term):
    """
    Return a tuple containing a queryset to implement the search
    and a boolean indicating if the results may contain duplicates.
    """

    # Apply keyword searches.
    def construct_search(field_name):
        ...

    ...

Empty and whitespace-only files are now normalized

Currently under the stable style, empty and whitespace-only files are not modified. In some discussions (issues and pull requests), the consesus was to reformat whitespace-only files to empty or single-character files, preserving the original line endings (LR/CRLR) when possible.

In the end, we settled on these rules:

  • Empty files are left as is
  • Whitespace-only files (no newline) reformat into empty files
  • Whitespace-only files (1 or more newlines) reformat into a single newline character

This was added to the preview style in 22.12.0.

Spyder cell separator comments (#%%) are now normalized

Black adds a space after the pound character in comments as per PEP 8. This caused trouble as many editors have special comments for various functionality (eg. denoting regions in the file) which could not contain any spaces.

To play with other tools, Black leaves Spyder’s special #%% comments (among other special comments) alone.

Sometime later, Spyder started to recognize # %% alongside #%%. So in Black 22.3.0, the preview style was changed to start adding spaces in #%% comments.

What is not being promoted?

Ideally I would’ve provided examples for these changes too, but I need to get this post out quickly and I’m getting tired. I’ve added links for further reading if you’re curious.

Experimental string processing (aka ESP) is not being promoted to the 2023 stable preview. There’s a lot of reasons why, but Jelle summarized it well in this comment:

[…] At this point there’s still half a dozen stability bugs with experimental string processing, so that alone is enough to block it from going into the 2023 stable style. However, I think we should consider dropping (most of) the feature entirely, instead of leaving it behind a flag forever, since there are so many stability bugs and often it arguably makes for worse output. That should be discussed in #2188, though.

And these three depend on ESP being promoted:

Additionally, two other changes are currently not slated for promotion and will remain in the preview style until 2024, mostly since they were landed too late in the year to allow for enough testing. These are:

“I want a new change to the stable style”

If you want a change that’s not covered by something in the preview style already (and can’t be achieved by simply tweaking a pre-existing style change), check the issue tracker for a issue that covers what you want. If you don’t find one, file an issue here or drop a comment here.

Although, if you’re hoping to squeeze in a new major style change into the 2023 stable style, that probably won’t be possible being so late in the year. It can always be promoted to stable for 2024.


  1. There’s some interesting history to why we added this flag, but I don’t have time to get into it right now. ↩︎