Why the sad face?
When you first encounter Black, a few things about it might surprise you. One of the those things might be "sadface dedent", the style in which closing parentheses in function signatures and other block headers are put on its own line. I arrived at this formatting style long before creating the auto-formatter. It’s got a few objective advantages.
Those advantages can be summed up as follows:
- consistency: all bracket pairs are treated equal;
- readability: the function signature (or
if
statement,while
test, and so on) is clearly delimited from its body; - developer efficiency: it minimizes diffs, allowing for faster code reviews and easier git history navigation.
Let’s look into each of those in more detail.
All bracket pairs are treated equal
The argument here goes like this: you already close all other bracket pairs in this manner. You do it for multi-line list and dictionary literals:
directives = {
'function': EQLFunctionDirective,
'constraint': EQLConstraintDirective,
'type': EQLTypeDirective,
'keyword': EQLKeywordDirective,
'operator': EQLOperatorDirective,
'synopsis': EQLSynopsisDirective,
'react-element': EQLReactElement,
'section-intro-page': EQLSectionIntroPage,
'struct': EQLStructElement,
}
You do it when calling functions:
if ret is DEFAULT:
ret = self._get_child_mock(
_new_parent=self, _new_name='()'
)
self.return_value = ret
You do it when importing many names:
from typing import (
Iterator,
List,
Sequence,
Set,
)
You do it to help with readability of complex boolean expressions:
create_flag = (
not self.disable_multitouch
and not self.multitouch_on_demand
)
It follows quite easily that you might want to do the same for blocks:
def getCommonAncestor(self, lnode, rnode, stop):
if (
stop in (lnode, rnode) or
not (
hasattr(lnode, '_pyflakes_parent') and
hasattr(rnode, '_pyflakes_parent')
)
):
return None
Did you notice? All of the code snippets above come from real-world Python projects. None were written by me and none are auto-formatted by Black. The point I’m making here is that this style isn’t some Black-specific eccentricity. And while I came up with it on my own in my programming journey, so did many other developers.
Moving on.
Clearly delimited block header from body
Let’s take this example:
# Before
if (not line or (line[0] == '#') or
(line[:3] == '"""') or line[:3] == "'''"):
self.error('Blank or comment')
return 0
Due to the number of parentheses in the if
test, it’s not obvious visually what the closing parenthesis right before the colon pairs with. It would also be impossible to see where the if
test ends without awkwardly 1-indenting the second line of the test to hint that this is not part of the body. Can we do better? Let’s see what Black would do:
# After
if (
not line
or (line[0] == "#")
or (line[:3] == '"""')
or line[:3] == "'''"
):
self.error("Blank or comment")
return
Wow, six lines instead of two? That was some explosion. But look at it again, is it easier to scan visually? Is it easier to see where the test is and where the body is? I think there’s no contest.
Another similar example:
# Before
if (comp != dotdot or (not initial_slashes and not new_comps) or
(new_comps and new_comps[-1] == dotdot)):
new_comps.append(comp)
And black-formatted:
# After
if (
comp != dotdot
or (not initial_slashes and not new_comps)
or (new_comps and new_comps[-1] == dotdot)
):
new_comps.append(comp)
It minimizes diffs
When you put the closing parenthesis in its separate line, you don’t have to remove it from the last element when adding a new one. Consider:
@@ -114,7 +114,8 @@ class MouseMotionEvent(MotionEvent):
with win.canvas.after:
de = (
Color(.8, .2, .2, .7),
- Ellipse(size=(20, 20), segments=15))
+ Ellipse(size=(20, 20), segments=15),
+ Depth(.5))
self.ud._drawelement = de
if de is not None:
self.push()
versus:
@@ -115,6 +115,7 @@ class MouseMotionEvent(MotionEvent):
de = (
Color(.8, .2, .2, .7),
Ellipse(size=(20, 20), segments=15),
+ Depth(.5),
)
self.ud._drawelement = de
if de is not None:
This advantage also requires keeping trailing commas at the last element. You can see this in the second example above.
In function signatures it neatly leaves space for a return type annotation
I don’t think anybody can claim that the "sadface dedent" is particularly beautiful when you look at a function signature like this:
class Command(struct.MixedStruct, metaclass=CommandMeta):
...
@classmethod
def _modaliases_from_ast(
cls,
schema,
astnode,
context,
):
modaliases = {}
if isinstance(astnode, qlast.DDLCommand):
for alias in astnode.aliases:
if isinstance(alias, qlast.ModuleAliasDecl):
modaliases[alias.alias] = alias.module
Functionally the advantage is that it clearly delimits the function signature from its body. But it further makes sense in the presence of type annotations:
class Command(struct.MixedStruct, metaclass=CommandMeta):
...
@classmethod
def _modaliases_from_ast(
cls,
schema: s_schema.Schema,
astnode: qlast.DDLOperation,
context: CommandContext,
) -> Dict[Optional[str], str]:
modaliases = {}
if isinstance(astnode, qlast.DDLCommand):
for alias in astnode.aliases:
if isinstance(alias, qlast.ModuleAliasDecl):
modaliases[alias.alias] = alias.module
By having the closing signature parenthesis dedented, we have more space for the return annotation and still provide an elegant separator between the signature and the body.
Can’t you just use a custom indentation level?
If you hug the closing parenthesis to the last line of an if
test (like in the “Blank or comment” example above), the test and the body will blend visually. This is enough of a problem that flake8
will complain:
E125 continuation line with same indent as next logical line
You can solve it by using a non-standard indentation level for this case but it’s problematic as you have to choose which non-standard level that should be. Smaller than 4 spaces would violate PEP 8, and a double 4-space indent is unnecessarily giving up precious horizontal space for actual logic in the if
test, making it more likely to have to be awkwardly split.
Conclusion
Sure, the disadvantage of the "sadface dedent" is that it looks alien on first encounter. It’s weird. And sure, beautiful is better than ugly. But form should follow function. The "sadface dedent" style is objectively better even if it’s subjectively uglier.
Acknowledgements
This has been proof-read by Hynek. Thanks!