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!

#Programming #Python