Skip to content

Allow reduced number of blank lines for dummy implementations #1797

@antonagestam

Description

@antonagestam

Hi!

I would like to start a discussion about reducing blank lines in some special cases, I hope this is welcome :)

The Blank Lines section in PEP8 states:

Blank lines may be omitted between a bunch of related one-liners (e.g. a set of dummy implementations).

While this is in the same paragraph as a sentence about functions, I would like to propose a liberal but well defined interpretation of this. It boils down to allowing reducing blank lines for empty classes and functions. This will make code that uses a lot of type definitions or overloads more readable. In the case of overloads this especially makes sense as it will allow overload definitions to be visually grouped with the overloaded function.

My opinion is also that the requirement for classes to take at least two lines and require being surrounded by two blank lines makes these definition feel heavy. Now talking about how code feels might seem misplaced and unscientific mumbo-jumbo, but I think it is important to allow class definitions to feel light when they're used in a Type-driven development style. Some of these usecases for class definitions that would benefit from feeling lighter are custom exceptions, and intersections. Why should a type definition like A = NewType("A", int) only require one line while a type definition like class Intersection(A, B): ... require six? I argue that making these definitions feel heavy will make developers refrain from using more elegant solutions that utilizes the type system over solutions that feel light but are in reality less expressive and doesn't describe business domains as fully as they could.

  1. Allow putting the entire definition on one line if the entire body consists of pass or ... (ellipses).
class CustomError(Exception): ...

def fn(a): ....
  1. Allow removing blank lines between empty classes and between functions, but require one blank line between an empty class and an empty function. Functions and classes with a docstring as the entire body will also be considered "empty", but not allowed to be defined on a single line.
class CustomError(Exception):
    """Allow documented classes to feel cozy too, as we don't want to discourage docs."""
class OtherError(CustomError): ...

def dummy(a): ...
def other(b): ...
  1. Non-dummy classes and functions should remain their current set of rules for surrounding blank lines. With the exception of overloads. The rule for overloads could be generalized as to allow functions with the same name to skip blank lines between them, but to require two blank lines surrounding them.
class Error(Exception): ...


class Implementation(Exception):
    """I am not an empty class and require some space"""

    a = 1


def fn(): ...


@overload
def a(arg: int) -> int: ...
@overload
def a(arg: str) -> str: ...
@overload
def a(arg: object) -> NoReturn: ...
def a(arg: Union[int, str, object]) -> Union[int, str]:
    if not isinstance(arg, (int, str)):
        raise TypeError
    return arg

While I think this could become well defined, and should before implementation work starts, I'm sure I've missed some edge cases that needs defining. Let's hammer them out if this is welcomed as a good idea in the first place, and not shot down ;)

Current behaviour

Example 2. is currently reformatted by black as (14 lines instead of 6):

class CustomError(Exception):
    """Allow documented classes to feel cozy too, as we don't want to discourage docs."""


class OtherError(CustomError):
    ...


def dummy(a):
    ...


def other(b):
    ...

Example 3. is currently formatted like this with black (33 instead of 22). Notice how information is lost here, the original formatting communicates to a reader that the overloads are related to each other:

class Error(Exception):
    ...


class Implementation(Exception):
    """I am not an empty class and require some space"""

    a = 1


def fn():
    ...


@overload
def a(arg: int) -> int:
    ...


@overload
def a(arg: str) -> str:
    ...


@overload
def a(arg: object) -> NoReturn:
    ...


def a(arg: Union[int, str, object]) -> Union[int, str]:
    if not isinstance(arg, (int, str)):
        raise TypeError
    return arg

Metadata

Metadata

Assignees

No one assigned

    Labels

    F: empty linesWasting vertical space efficiently.S: acceptedThe changes in this design / enhancement issue have been accepted and can be implementedT: styleWhat do we want Blackened code to look like?

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions