Over the years of using Python, I’ve repeatedly come across a code formatting issue that I find quite distracting, although it seems to be hardly ever discussed. I’ve finally written a Flake8 plugin that partially solves it (at least for the projects I work on), but I’m curious if this would be something worth specifying in PEP 8, especially since the formatting in question appears also in the Python standard library.
The issue arises from a clash between these two commonly used rules:
To avoid spaces around the equals sign in function calls and definitions (if unannotated): foo(bar=a).
To put spaces around binary operators: a + b.
When we apply them simultaneously, we get foo(bar=a + b). This intuitively feels wrong to me, as the operator precedence works the other way than the spacing suggests (something like 1+2 * 3 would be similarly confusing). Or, to put it differently, the spaces in the expression seem to completely defeat the purpose of not putting the spaces around = in the first place, thus making the whole formatting look illogical.
Although PEP 8 already mentions that spacing should follow operator precedence (“If operators with different priorities are used, consider adding whitespace around the operators with the lowest priority(ies)”), the above case is never directly called out as wrong in the examples, and it’s not clear if the equals sign is considered an operator at all. Therefore, Python linters and formatters happily output code like this, and their maintainers reject new rule suggestions on this matter (ruff#20471, pycodestyle#1240).
Practical examples of the questionable formatting can be found in multiple places, like documentation of external Python libraries (Django: [1], [2]; Celery: [3]) or the source code of CPython ([4], [5], [6]).
Is this the expected result of the aforementioned styling rules (but if so, why do we have the exception for a very similar case of type-annotated function arguments), or is this an omission that should be clarified?
Feel free to format such constructs however you prefer. You are not obliged to follow PEP 8.
I agree with the linter authors that commented on the issues you linked - changing this would be extremely disruptive to people who do currently use those rules, for very little benefit. You can simply disable the relevant rules if you want to use the linters but disagree with that rule.
On a personal note, I agree that the formatting is a little awkward. But I’m not convinced there is any rule that would be better in all cases, and this should be a matter of judgement on a case by case basis. In general, I don’t find linter rules that enforce style choices to be particularly useful, so I avoid them myself. But I’m very aware that I’m an outlier in this.
I can clarify that part for you, at least: It’s not an operator. In general, an operator does the same sorts of things that a function can; a unary operator like -x is like calling a function with one argument negate(x), a binary operator like a + b is like calling a function with two arguments add(a, b), and similarly if you have more. An operator is found inside an expression and is a part of that expression. Python’s keyword arguments can’t be treated like that; the function call as a whole is an expression, and the arguments are themselves expressions, but you can’t package up the arguments as a “thing” and then apply that to the function call. You can’t replace foo(bar=a) with foo(args) - there is no value for args that would result in the behaviour of bar=a.
(The equals sign isn’t an operator when used for assignment either (at least, not in Python); a = b is not an expression and cannot be passed to a function.)
So the rule about operator precedence doesn’t apply here, giving you a number of viable options:
Keep it all compact. foo(bar=a+b)
Space out the expression. foo(bar=a + b) As you say, this looks weird.
Break it onto multiple lines. Give it a try, see if you like it, probably not.
Ignore PEP 8 and write foo(bar = a + b). Personally I wouldn’t - I think this makes it look too much like an expression involving an existing name bar - but PEP 8 is not a millstone around your neck. You are free to format your code however you choose.
Stick the value a + b into a variable and then write foo(bar=total)
Build the dictionary {"bar": a + b} and pass that in as foo(**args)
Write lists of options like this in stream-of-consciousness style and then realise that they’re in a weird order.
The right choice will depend on the code you’re writing, and particularly on how complex the expression is. Personally, I keep this exact example compact foo(bar=a+b) because, in my opinion, it’s short enough to not need the binary operator to be spaced out. But if there were more steps, like foo(bar=a+b*c), I’m not sure. It might be worth putting it in a variable, or maybe I’d break it over several lines:
foo(
bar=a + b*c,
)
especially if there are other keyword arguments being passed. Fortunately, we have the freedom to do things any which way we like, and to change our minds later and reformat the code I don’t remember who said it, but the disgust you feel when looking at your old code is proof of how much you’ve grown as a programmer since writing it. I’ve been coding for several decades now, and I still change my mind on formatting questions now and then
It’s not as easy as disabling the E251 rule in the projects I work on, as other team members prefer to keep it for consistency with other Python code. That’s why I decided to create the custom plugin, as it only reports that awkward formatting, without producing conflicts with the standard. In most cases, putting parentheses around the expression is good enough of a solution to improve readability without refactoring code.
And I wouldn’t call this new rule “extremely” disruptive. That formatting doesn’t actually appear that often. Most of the time we don’t pass expressions as keyword arguments or parameter defaults. I guess this is why it was not covered in PEP 8.
If your team has different views on correct formatting, then you have a different problem, and not one that linters or style guides can help with.
You might not, but I’m sure many, many other projects do. And you can’t expect them to ignore a change to the PEP 8 rule, if you’re not willing to ignore the current rule…
I’ve already considered many of these options (some are listed in the readme of the linter plugin i linked to). And another one, putting parentheses around the expression like this: foo(bar=(a + b)). I find this one the most universal, although it may not look best in this contrived example.
As for breaking the arguments into multiple lines - this makes them more readable, but doesn’t actually solve the core of the issue, which is that not putting spaces around =while putting spaces inside the expression looks illogical/weird. So even with multiple lines, I’d still use the parentheses or remove the spaces in the expression.
If your team has different views on correct formatting, then you have a different problem, and not one that linters or style guides can help with.
It’s more like: they are not bothered by this formatting, but also don’t mind having the new linter rule to improve it, while without the linter they would easily forget about it.
You might not, but I’m sure many, many other projects do. And you can’t expect them to ignore a change to the PEP 8 rule, if you’re not willing to ignore the current rule…
I actually meant “we as Python programmers”. I’m basing this on how many examples I found in the Python standard library. A few more than I linked to, but certainly not too many. So this makes me think that a rule clarification would not be disruptive, and could easily be applied in projects following PEP 8.
I’m sure people will comment loudly on any change, but I doubt the “extremely disruptive” aspect. What I mean is that an age of near-ubiquitous linting, widespread auto-formatting and .git-blame-ignore-revs, all it takes to fix things is a single, automatable and ignorable commit to adapt to some new rule.
Since it makes no functional difference, there’s also no knock-on effect, like some API or behaviour change would have. Ironically, the fact that it’s something as comparatively unimportant as formatting is probably the biggest disruptor, because people get very passionate about cosmetic issues (the bike-shed phenomenon).
Personally, I’ve always disliked the phenomenon the OP points out – enough to think it’d be worthwhile to fix, but not enough to wade into this fight myself…