PEP 8: More Nuanced Alignment Guidance

I’ve seen some other threads explain that PEP 8 is more of a guidance on how Python code should be formatted, not necessarily a requirement. (Granted I think it is a requirement now to contribute to the std library.)

While I agree that PEP 8 does not require anyone outside of project contribution to adhere to the recommendation, the majority of IDEs and projects work off of PEP 8. Because PEP 8 represents a standard to follow that encourages clean, readable, maintainable code, I would like to suggest a tweak that could increase the readability of a lot of code without much cost.

I’m really curious what others in the community think about this :slight_smile:

Background

The current guidance in PEP 8 discourages extraneous whitespace for aligning assignments or other constructs, prioritizing simplicity and avoiding unnecessary visual clutter over alignment for purely aesthetic reasons. While this guidance works well in many scenarios and is easy to apply, there are cases where alignment significantly improves code readability, particularly in settings with related variables or structured data.

I want to propose an update to Python’s style guide regarding the use of whitespace for aligning assignments and similar constructs. The goal is to provide more nuanced guidance that balances readability and simplicity while discouraging excessive alignment practices that may lead to overly complex formatting. The proposed changes address the community’s growing need for clarity on this topic, as evidenced by feature requests in popular tools like PyCharm. Note the number of issues that are similar, duplicates, and related.

I know unrestricted alignment can lead to overly complex formatting, making code harder to maintain and even read in some cases. This proposal seeks to strike a balance by refining the guidance to allow alignment in specific scenarios where it enhances readability, without encouraging overuse.

Current Example Discouraging Alignment from PEP 8 (as of 2025-01-03)

x             = 1
long_variable = 2

PEP 8 currently marks this as incorrect, recommending the simpler:

x = 1
long_variable = 2

Issue:

While the above guidance discourages unnecessary alignment, it does not consider cases where alignment can improve readability by enabling cognitive grouping of related variables, making the structure and relationships between values clearer at a glance, such as:

min_value = 0
max_value = 100
average   = 50

Here, alignment groups related variables and enhances visual structure.

Proposal

  1. General Principle: The primary goal of alignment should always be to enhance readability. Avoid alignment if it introduces complexity or makes code harder to maintain.

  2. Allow Grouped Alignment: Alignment is permissible within logical groups of related variables where it improves clarity. For example:

    width  = 800
    height = 600
    depth  = 256
    

    However, avoid aligning across unrelated code blocks or sections.

  3. Limit Scope: Avoid excessive alignment or overuse, such as aligning across significantly different variable lengths or large code blocks. Consider breaking variables into shorter, logical groups if alignment spans more than a few lines.

Backwards Compatibility

The proposal does not mandate alignment but refines guidance. Existing codebases remain “compliant” if they follow current PEP 8 recommendations. This gives IDEs the freedom to implement the requested features while still being “in compliance” with PEP 8.

More Examples

Good Alignment:

Two Variables:

x  = 10
y  = 20

Three Variables:

name       = "Alice"
age        = 30
occupation = "Engineer"

Six Variables:

red    = 255
green  = 0
blue   = 0
alpha  = 1.0
width  = 800
height = 600

Good and Bad Examples for Complex Assignments:

Good Example:

val_set           = get_value_set(vals)
thread_safe_count = ThreadSafeInt(len(val_set))
chunk_size        = thread_safe_count / len(THREAD_COUNT)
val_chunks        = divide_by_chunks(val_set, chunk_size)
threads           = list()
printers          = math.ceil(THREAD_COUNT * 0.33)
start             = time.time()

or

val_set           = get_value_set(vals)
thread_safe_count = ThreadSafeInt(len(val_set))
chunk_size        = thread_safe_count / len(THREAD_COUNT)
val_chunks        = divide_by_chunks(val_set, chunk_size)

threads  = list()
printers = math.ceil(THREAD_COUNT * 0.33)
start    = time.time()

or

val_set    = get_value_set(vals)
thread_safe_count = ThreadSafeInt(len(val_set))
chunk_size = thread_safe_count / len(THREAD_COUNT)
val_chunks = divide_by_chunks(val_set, chunk_size)
threads    = list()
printers   = math.ceil(THREAD_COUNT * 0.33)
start      = time.time()

Bad Example:

val_set = get_value_set(vals)
thread_safe_count = ThreadSafeInt(len(val_set))
chunk_size = thread_safe_count / len(THREAD_COUNT)
val_chunks = divide_by_chunks(val_set, chunk_size)
threads = list()
printers = math.ceil(THREAD_COUNT * 0.33)
start = time.time()

The bad example lacks alignment, making it harder to visually parse related variables and their values.

Avoid Overuse:

Two Variables:

short = 1
very_very_long_variable_name = 2

Instead, prefer:

short = 1
very_very_long_variable_name = 2

Three Variables:

id                      = 12345
short_name              = "A"
very_long_variable_name = "Example"

Instead, prefer:

id = 12345
short_name = "A"
very_long_variable_name = "Example"

Group large sets by length:

var1   = "A"
var2   = "B"
var3   = "C"
long_name_1 = "D"
long_name_2 = "E"
long_name_3 = "F"

Instead, group logically:

var1 = "A"
var2 = "B"
var3 = "C"

long_name_1 = "D"
long_name_2 = "E"
long_name_3 = "F"

Sorry for the long read, thanks for getting here :slight_smile:

1 Like

PEP 8, while used by the wider community, is intended for use within the standard library. As such, making changes to it because other tools might benefit is out of scope.

I mention “tools”, not “code”, because the only way to “increase readability of [existing] code” would be to beat the maintainers of that code with some enforcement tool that would lint or auto-format it to your liking. Otherwise a wording change in a PEP does nothing. Unless the plan is to decry tools that lint or format hand-aligned assignments away on the basis of PEP 8 non-compliance once the change to the document is introduced?

As you know, PEP 8 already starts with a safety disclaimer where consistency within your own code trumps any prescribed rules. For a group of assignments where you feel like hand-formatting really is that important, format them to your liking. Disable the linter/auto-formatter in that block and you can move on.

I’m -1 to changing the PEP.

9 Likes

Now add a new one called printers_per_thread and what do you have to do? Edit every other line to add some extra unnecessary whitespace.

To be quite honest, I am happy that people disagree with PEP 8. It encourages diversity of style guides. It encourages projects to develop their own rules, instead of going “PEP 8 is the one true set of rules, we must follow it”. Perhaps this is the impetus for you (or your project/organization) to separate yourself from following PEP 8.

In my personal opinion, every serious Python programmer should be at least broadly familiar with PEP 8’s rules and why they exist, but it is absolutely NOT meant to be a set of shackles that force you to write your code in a particular way. Do what’s best for your project. For example, I use tabs for indentation everywhere, even though that’s not what PEP 8 says; and I sometimes do the “definitely not” of putting a try/finally or if/else on two lines with their bodies. And that’s completely okay. If you want to space out your assignments like this, you go right ahead! It’s your project and you’re in charge.

9 Likes

As someone who loves aligning assignment statements like this, I don’t think this should be included in added to PEP 8. It just adds more “attack surface” for unproductive style debates. The urge to appeal to the supposed authority of PEP 8 (and forgetting its “A Foolish Consistency is the Hobgoblin of Little Minds” section) has caused more heat than light, in my experience.

Like Chris says, PEP 8 is not meant to be a set of shackles, and I don’t think we should add more links to it.

EDIT: To be clear, what I mean is I don’t want PEP 8 to be modified either way about this topic. Fiddling with PEP 8 isn’t going to be productive use of time.

8 Likes

That’s a very sensible position. Before we remove this from PEP 8, what do typical tools (like Black and – hm… what else is there?) do? If they enforce this, I think we should not change PEP (since in practice you have to argue with the tools authors instead of with the PEP authors – I know Black doesn’t always slavishly enforce PEP 8).

I take it back. As usual, this discussion is going nowhere. I prefer not to delete my post (since that somehow tends to cause more confusion) but I withdraw it, and I am muting this conversation.

3 Likes

This is subjective. In my opinion, it is more readable, even if it doesn’t look aesthetically pleasing. However, it is not unreadable. Code is about functionality and clarity, not symmetry or appearance.

This is difficult to                                 read:

Also note that PEP 8 enforcement tools can be configured.

2 Likes

Well I mean, yes it is, but yes it also is; but not everyone agrees on what constitutes “clarity” and “appearance” :slight_smile:

1 Like

I think it depends on what you’re used to reading most likely. I experience more physical eye strain with the “Bad Example” :sweat_smile:

I agree with you, I don’t think PEP 8 should change based on how other tools implement their style guidelines. I get that the suggestion isn’t necessary because PEP 8 already says to choose the style that makes sense. Since the documentation already goes into shoulds and should-nots, then elaborating on those should probably be allowed. Otherwise, PEP 8 is forever set in stone. Is that the intention?

This suggestion was more of a slight tweak to create more flexibility and further empower developers to choose the style that is most readable in their code. From my perspective, it would increase the amount of acceptable, readable, styles within the standard library instead of shrinking it.

Granted styling is very subjective anyways

probably rhetorical, but I would do something like this

val_set           = get_value_set(vals)
thread_safe_count = ThreadSafeInt(len(val_set))
chunk_size        = thread_safe_count / len(THREAD_COUNT)
val_chunks        = divide_by_chunks(val_set, chunk_size)

threads  = list()
printers = math.ceil(THREAD_COUNT * 0.33)
start    = time.time()
printers_per_thread = somefunction()

I appreciate the openness to diverse thoughts and opinions! I hope my suggestion is actually encouraging more diverse/readable styles within the standard library and community as a whole. It’s adding a nuisance to the style guide instead of a blanket right-wrong.

autopep8 for example.

2 Likes

If we change something in PEP 8 then every formatter and linter will be stuck in the ultimatum of “do we cease to follow PEP 8?” or “do we adopt the change and fail everyone’s existing code?”. I know this specific proposal dodges that problem by being strictly allowing a new thing without changing or restricting anything else but I’d still feel a lot more comfortable if PEP 8 was forever considered immutable.

1 Like

I’d feel a lot more comfortable if PEP 8 was recognised for what it is - guidance, not rules, specifically for the CPython codebase (but offered freely for others to use if they so choose) :slightly_smiling_face:

This is not a dig at black, or ruff, or any of the other linters and auto-formatters out there. They are tools that people can choose to use or not, as they please. It’s a dig at the mindset of people who view violating PEP 8 as somehow inherently wrong, rather than a simple style choice.

18 Likes

An often overlooked part of PEP 8 I like is the advice to ignore a guideline if applying it “would make the code less readable, even for someone who is used to reading code that follows this PEP.” If you haven’t yet, I encourage meditating on that for a while.

The text of @Anthony’s proposal is a nice elaboration on this, and when read that way, I agree with it. What I don’t agree with is the examples that focus on assignment.

When I see code like the OP’s:

… I wonder what’s being pointed out here. The fact that these are all assignments? How is that important?
Those operations are all independent, so I think the unaligned variant is more readable – especially if you think about “readability” as “how well the code expresses its meaning”, rather than aesthetics (or just being used to a particular style).

One of the examples downright misleading, IMO:

red    = 255
green  = 0
blue   = 0
alpha  = 1.0

Three of these colour coordinates are ints between 0 and 255, and one is a float between 0 and 1. I might want to bend formatting guidelines here, but to highlight the difference, not to make them look more similar.
I wouldn’t mind seeing code like this:

red   = 255
green =   0
blue  =  64

alpha =   1.0

width  = 1024
height =  768

But then, if you have a bunch simple assignments and they’re becoming unreadable without alignment, it’s probably time to think about data structures or config files. Alignment would likely be the proverbial lipstick on a pig.

Here’s what I’d put as a “good” example of alignment – that is, ignoring the guidelines to highlight structure/symmetry:

def turn(velocity, turn_direction):
    dx, dy = velocity
    match turn_direction:
        case LEFT:    return -dy,  dx
        case RIGHT:   return  dy, -dx
        case FORWARD: return  dx,  dy
        case BACK:    return -dx, -dy
14 Likes

… and I suppose the CPython repo should be free change its own style guide without being hamstrung by what others will do with that change. Fair enough, I take back my previous comment.

3 Likes

I agree with Petr, although I’d be fine with grouping RGBA because they often go together. Similarly for HSL (H float 0-360, S and L float percentage 0-100), HSLA (A float 0-1), HSV and HSVA, etc. And then width and height in another group. But this is a nitpick and would depend on the codebase and team’s choices.

PEP Editor hat: most PEPs generally aren’t changed once “finished”, especially standards track PEPs. However, PEP 8 is an Active Process PEP and not set in stone. For example, it was changed in 2024 and 2023. See PEP 1 for more info.

Are you asking for tools to explicitly enforce this style guideline, so that you can disable it if needed?

More than one space around an assignment (or other) operator to align it with another:

# Correct:
x = 1
y = 2
long_variable = 3

# Wrong:
x             = 1
y             = 2
long_variable = 3

I couldn’t find a specific rule to disable this. If there is one, please let me know. I haven’t reviewed the rule sets of other tools.

1 Like

These “good” examples look bad to me. I am ready to accept aligned assignments if you have a long list of initialized constants (with not too long names). I am glad that PEP 8 is liberal enough and allows aligned assignments if appropriate, but I would be sad if it made suche style preferable.

@Rosuav has already brought a good counterargument – if you add a new assignment with a very long name, would you reformat all other assignments. We already had such issues with new long opcode names.

For me personally, there is another problem. I have a very narrow field of vision – I can’t see the whole long line and more than a few lines at a time. I read a line in parts – the beginning, the middle, the end. If the middle is empty, I can miss a line. I can miss that there is something at the end of that line or lose right connections between the beginnings and the ends. Also, when I move my eyes from the end of a line to the beginning of a line, I can lose which line is next, especially if the beginnings are similar. I have to reread or count the lines, or highlight the beginning of the line by text selection. In the worst case, I can skip several lines and not notice this.

Therefore, @ahanel13’s proposal, if accepted, will hit me hard. I understand that this is only my problem.

9 Likes

I completely agree. In Pycharm PEP8 checks are enabled by default. I found them very annoying at the beginning, but lately I started to appreciate them. This does not stop me to disable them when I do not agree, like E251 or E402.

About the OP requests, the other responses already said much. I would only add that alignment is particularly tedious if you work on someone else code. So, if you have co-workers, please don’t.

This is IMO the biggest argument against alignment; it’s a time-wasting trap. It might look nice aesthetically but practically once you start aligning stuff you’ll find yourself constantly fiddling with whitespace and (re-)grouping assignments together, hopefully based on their semantic closeness but realistically based on variable name length. And once name length comes into play you’re now also wasting time thinking of names (both for new and existing vars) with lengths that better fit the others in their arbitrary group of assignments. Anyone who is even a little bit obsessively meticulous will end up wasting hours and hours in total on that ultimately trivial activity; I’ve been there.

Oh, and don’t even think about what this does to commit size and blame history. Suddenly a simple variable rename has to touch multiple nearby lines, effectively discouraging an activity that actually matters - picking a clear and succinct name.

8 Likes