Python Auto Formatter: Autopep8 vs. Black

Autopep8 and Black are both great tools to auto-format your Python code to conform to the PEP 8 style guide. Learn the key differences between the two.

Written by Shangjie Lyu
Published on Oct. 05, 2023
Developer writing python code on a laptop
Image: Shutterstock / Built In
Brand Studio Logo

Autopep8 and Black are both great tools to auto format your Python code to conform to the PEP 8 style guide. Black is the most popular tool of its kind based on GitHub activity, while autopep8 is slightly less popular.

Autopep8 vs. Black Explained

  • Autopep8: Autopep8 is an auto formatter for Python that edits code to align with the PEP 8 style guide. It only formats the code that’s out of alignment with the style guide.
  • Black: Black is an auto formatter for Python that converts the entire codebase into its own style to ensure it aligns with the PEP 8 style guide.

One of the major differences is that Black is an opinionated formatter, meaning that it always converts the entire codebase into its own style, whereas autopep8 preserves the input style to some extent and only fixes the necessary parts.

I’ve used both tools, and I’d like to share with you why I prefer Black over autopep8.

 

What Is Autopep8?

Autopep8 is a Python tool that automatically formats code to match the PEP 8 style guide. It differs from the auto formatter Black in that it only removes the necessary parts of code that aren’t aligned with PEP 8. 

More on Python__new__ vs. __init__ Methods in Python

 

Autopep8 Disadvantages

Don’t get me wrong, autopep8 is a great tool, and I like it. The best thing about autopep8 is that it allows plenty of user configurations, whereas Black barely allows any. However, I’ve come across some problems with autopep8 that are enough to keep me from using it.

 

1. It Aggressively Sorts Imports

Some may see this as a good thing, however, a good tool should focus on only one thing. As a formatting tool, it shouldn’t try to change the order of the code, and it can sometimes cause problems when it does.

Let’s consider the below example. Although sys.path.append is generally not good practice, let’s say we really want to do something first before importing the rest of the modules.

 

import sys

sys.path.append("some_path")

import some_module

Autopep8 would rewrite the script into the following:

import some_module
import sys

sys.path.append("some_path")

And this is problematic. In another example, let’s say we want to enable the back end of matplotlib for a Linux terminal. We would need to set the back end matplotlib.use("agg") before the pyplot import from matplotlib import pyplot as plt.

In contrast, Black will not change the example code above. Black only does formatting while the semantics of the code remain exactly the same. In other words, it doesn’t sort your imports and doesn’t change the order of your code. We can leave the task of sorting imports for another great tool, isort.

You can add # nopep8 to solve this issue to some snippets of your code, which explicitly tells autopep8 not to touch it:

import sys

sys.path.append("some_path") # nopep8

import some_module

It works in this case, but I found that the # nopep8 exclusion rule is not applied to comments. So, autopep8 would still convert the following ### some comment # nopep8 into # some comment # nopep8.

On the other hand, Black does not edit comments and docstrings, and you can exclude some of your code from being formatted by adding two comment lines # fmt: off and # fmt: on before and after your code block.

Anyway, it’s not very Pythonic, right?

 

2. It Doesn’t Properly Enforce Indentation 

Autopep8’s default indentation is set to the same as your editor’s tab size, and you can specify the value with the --indent-size argument. Autopep8 only uses this input value to set the indentation for the beginning of each statement, but the indentation within brackets is always set to the default tab size.

Let’s look at this example, where the code indentation is one, and my editor’s default tab size is four.

 

def add(a, b):
 return a + b

print(
 'some really long message that the formatter would not try and put the whole print statement in one line'
)

matrix = [
 [1, 2, 3],
 [4, 5, 6],
 [7, 8, 9]
]

After setting indentation to three with autopep8 autopep8 --indent-size 3 -i format_02_raw.py (-i means in place), we get the following:

def add(a, b):
   return a + b


print(
    'some really long message that the formatter would not try and put the whole print statement in one line'
)

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

We can see that the indentation in the first function is set correctly to three, but everything within brackets, in this case, the print statement and the matrix, are set to the default tab size of four.

I found this out because the default tab size in my work environment is two, and I prefer the indentation of four. It might sound trivial, but why risk the unnecessary inconsistency? Black doesn’t allow you to configure the indent size, as it always sets indentation to four in all places, which I don’t mind at all.

 

What Is Black?

Black is an auto formatting tool for Python that edits code to align it with the PEP 8 style guide. It differs from autopep8 in that it formats the entire codebase to its own style.

 

How to Use Black 

Black implements its own style, which some people like and some people don’t. I like it as it’s generally very clean and readable, but there are two things to keep in mind.

 

1. Use a trailing comma

The examples below show one of the most common reasons why some people don’t like Black. Here’s what the code looks like before formatting:

matrix = [
    [1,2,3],
    [4,5,6],
    [7,8,9]
]

def some_random_function(
    param_a: str,
    param_b: int = 1,
    param_c: bool = False
):
    ...

After formatting, without trailing commas:

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

def some_random_function(param_a: str, param_b: int = 1, param_c: bool = False):
    ...

It’s annoying, but before you get mad, let’s make some small changes to the code and see how Black changes its behavior. Here it is before formatting, with trailing commas:

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9],]

def some_random_function(
    param_a: str,
    param_b: int = 1,
    param_c: bool = False,
):
    ...

After formatting, with trailing commas:

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]

def some_random_function(
    param_a: str,
    param_b: int = 1,
    param_c: bool = False,
):
    ...

Yes, Black uses trailing commas to decide whether the items will be wrapped together or stay in new lines. If an array-like item ends without a trailing comma, whether it’s a list, matrix or dictionary, black always tries to warp it into one line. If it exceeds the length of the line, black then puts its contents in new lines and adds a trailing comma for you. If there is a trailing comma in the item, black makes sure its contents are separated into new lines.

In short, there will always be a trailing comma wherever an array-like item’s contents are in new lines. This makes sense, and in general, trailing commas are good practice as they make the code easier to maintain, as well as generate clean git diff when you make changes. So, if you don’t want black to wrap your code into one line, add a comma at the end.

Bonus tips: when I write SQL queries, I would write:

SELECT column_a
      ,column_b
      ,column_c
FROM some_table

Instead of this:

SELECT column_a,
       column_b,
       column_c
FROM some_table

I do that for the same reasons. It makes it easier to add or delete items and makes the code easier to maintain.

 

2. Specify Line Length 

The only thing I don’t like about the Black code style so far is regarding long if-statements, as the below example shows. Here it is before formatting:

def some_function():
    if some_relatively_long_statement_on_input and another_relatively_long_statement_on_input:
        ...

After formatting:

def some_function():
    if (
        some_relatively_long_statement_on_input
        and another_relatively_long_statement_on_input
    ):
        ...

It enforced consistency but sacrificed readability. Black’s default line length is 88, but sometimes I do have slightly longer statements, and I don’t want them to be formatted that way.

My solution is to allow a slightly longer line length. We can specify the line length using the --line-length or -l argument, and if we set the line length to 100, black -l 100 format_05_raw.py, the above example will not be reformatted. From my experience, a 100-ish line length would suit most long statements while still retaining good code readability. Although, you should really consider rewriting your statement if it’s over 100 characters long. Of course, this is up to each team to decide.

Autopep8 also has such an option --max-line-length, however, as autotpep8 tends to preserve the original code style, the formatting result is much less sensitive to the specified line length than black.

 

3. Use Black With Isort

Black doesn’t sort your imports, but we can use isort to do this. Let’s look at a quick example of what isort can do. Here is the code before isort:

import sys

sys.path.append("some_path")

from my_lib import Object

from my_lib import Object3

from my_lib import Object2

from third_party import lib15, lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9, lib10, lib11, lib12, lib13, lib14

from __future__ import absolute_import

from third_party import lib3

print("Hello World!")

After using isort:

import sys

sys.path.append("some_path")

from __future__ import absolute_import

from my_lib import Object, Object2, Object3
from third_party import (lib1, lib2, lib3, lib4, lib5, lib6, lib7, lib8, lib9,
                         lib10, lib11, lib12, lib13, lib14, lib15)

print("Hello World!")

It’s neat, and it doesn’t mess up our sys.path.append example.

There are some slight differences between how isort and Black organize imports, and we can set isort’s --profile option to black to sort the imports conforming to the Black code style, isort --profile black format_06_raw.py; we can also specify the line length with the -l argument.

We now have a pretty good workflow: use isort to sort the imports first, and then use Black to format the code. We can combine the two steps together in Makefile:

format:
 isort --profile black -l 100 src/
 black -l 100 src/

And now we can simply use make format to achieve everything discussed above (on an imaginary src/ folder).

A tutorial on how to use Python auto formatter Black. | Video: Carberra

More on Python 5 Ways to List Files in Python Directory

 

Should You Use Black or Autopep8?

Here’s the conclusion: I prefer Black over autopep8, but there are some practical tips that you should keep in mind when using Black, and it works best in parallel with some other tools like isort.

Explore Job Matches.