112

I have defined a function with a long list of arguments. The total characters in definition is above 80 and doesn't abide by PEP8.

def my_function(argument_one, argument_two, argument_three, argument_four, argument_five):

What can be the best approach to avoid horizontal scrolling?

Waldir Leoncio
  • 10,853
  • 19
  • 77
  • 107
Sudip Kafle
  • 4,286
  • 5
  • 36
  • 49
  • 1
    And what is the best way to write function call statement? :) – Nabin Dec 17 '17 at 12:02
  • 1
    The wide variety of answers here reinforces the fact that you should agree with your team on one style and stick to it! – F.M.F. Aug 05 '21 at 09:29

7 Answers7

157

An example is given in PEP 8:

class Rectangle(Blob):

    def __init__(self, width, height,
                 color='black', emphasis=None, highlight=0):

So that is the official answer. Personally I detest this approach, in which continuation lines have leading whitespace that doesn't correspond to any real indentation level. My approach would be:

class Rectangle(Blob):

    def __init__(
        self, width, height,
        color='black', emphasis=None, highlight=0
    ):

. . . or just let the line run over 80 characters.

BrenBarn
  • 242,874
  • 37
  • 412
  • 384
  • 12
    I was actually in confusion to choose between these two. For the first one, continuous lines have arbitrary whitespaces which is something which I don't prefer. The second one looks confusing to be a funciton. I would prefer the PEP8 official answer. – Sudip Kafle Jul 28 '14 at 07:31
  • 3
    I find the method in your second example dirty. Declaration in the first one catches my eye no-problem, while with the second one I have to look closer to find it. Especially when there are 20 methods with 2-3 lines each in the same class. – Błażej Michalik Aug 31 '16 at 13:03
  • I agree, 80 chars is too narrow. Make the lines a little longer and use more clear var/func names instead of ugly abbreviations/line-breaks. 100 chars should be standrad IHMO. – Rotareti Jan 11 '17 at 12:24
  • 10
    This is not about being more or less dirty, this is about the method on the second example is actually more practical. It allows the editor to make proper code folding of the method body while allowing you to see the parameters of the method. The official one as the author suggest doesn't correspond to any indentation level. – BPL Nov 05 '17 at 20:38
  • 2
    Potentially a bit off-topic, but does anybody know if there's a way to force flake8 to fail when it finds the first solution? I really dislike it even though it's standard PEP8 and prefer the second solution much more. – Ariel May 16 '19 at 07:29
  • 1
    If you can, blacklist the "E128" pep8 rule in your linter, if that doesnt work better to write your own custom rules @Ariel – Salyangoz Jan 09 '20 at 15:39
  • @Ariel There are a number of ways to do so. One option is flake8-black, but it will enforce a full vertical style. – DylanYoung Jul 07 '20 at 18:58
  • I couldn't find the first example in the PEP-8 docs. Did it get replaced? – LondonAppDev Jul 13 '20 at 20:29
  • The first option will also not help you if the method name is quite long. – HerpDerpington Aug 19 '23 at 10:47
53

For Python code that uses type annotations, I suggest this:

def some_func(
    foo: str,
    bar: str = 'default_string',
    qux: Optional[str] = None,
    qui: Optional[int] = None,
) -> List[str]:
    """
    This is an example function.
    """
    print(foo)
    ...

If you use yapf you can use these options in .style.yapf:

[style]
dedent_closing_brackets = true
split_arguments_when_comma_terminated = true

If you use black you don't need to do anything, it uses the above style out of the box, if you add a trailing comma behind the last parameter.

Rotareti
  • 49,483
  • 23
  • 112
  • 108
  • I would do this regardless of whether you're using annotations. It's both more maintainable and more legible. – DylanYoung Jul 03 '20 at 18:50
  • NOTE: I wasn't able to get YAPF to conform to this style. I've come to the conclusion that it's a fundamental problem with the way YAPF is designed: bin packing. Bin packing is largely the opposite of what you want to do to format code. – DylanYoung Jul 07 '20 at 19:00
  • This still looks weird to me but it's my favorite among alternatives I've seen. Only thing is it doesn't separate well from the body of the function without a docstring, but that also makes me more likely to include doctstrings which I should be doing anyway – rwalroth Jan 06 '22 at 18:52
30

1. What I would recommend

Even though it makes the function more verbose, for more than one argument, I think this is best — the example below is from Python —:

def my_function(
    argument_one, 
    argument_two, 
    argument_three,
    argument_four, 
    argument_five,
):
    ...

2. Why?

  1. Having each argument on one line makes it very simple to use git diffs, since changing one variable will only show that change. If you have more than one argument on each line, it's gonna be more visually annoying later.
    • Notice that the trailing comma after the last argument makes it easier to add or remove an argument later, while also conforming to the PEP 8's Trailing Comma Convention, and yielding a better git diff later.
  2. One of the reasons I really dislike the "align the arguments with the opening parenthesis" paradigm is that it doesn't yield easily maintainable code.
    • Kevlin Henney explains it is a bad practice in his ITT 2016 - Seven Ineffective Coding Habits of Many Programmers lecture (around 17:08).
    • The major reason it is a bad practice is that if you change the name of the function (or if it is too long), you're going to have to re-edit the spacing on all the arguments' lines, which is not at all scalable, though it may be (sometimes) (subjectively) prettier.
    • The other reason, closely related to the one immediately above, is about metaprogramming. If the codebase becomes too big, you will eventually find yourself needing to program changes to the code file itself, which might become hell if the spacing on arguments is different for each function.
  3. Most editors, once the parentheses are open and you press enter, will open a new line with a tab and shove the closing parenthesis to the next line, untabbed. So, formatting the code this way is very quick and kind of standardized. For instance, this formatting is very common in JavaScript and Dart.
  4. Lastly, if you think having each argument is too unwieldy because your function might have a lot of arguments, I would say that you're compromising easy formatting of your code due to rare exceptions.
    • Don't legislate by exceptions.
    • If your function has a lot of arguments, you're probably doing something wrong. Break it into more (sub)functions and (sub)classes.

3. Anyway...

A good convention is better than a bad one, but it's much more important to enforce one than be unnecessarily picky about them.

Once you've decided to use a standard, share your decision with your colleagues and use an automated formatter — for example, Prettier is a popular choice for JavaScript in VS Code; and the Dart language has standardized one globally: dartfmt — to enforce it consistently, diminishing the need of manual editing.

Philippe Fanaro
  • 6,148
  • 6
  • 38
  • 76
21

Personally I also used to come up with the same solution as @BrenBarn 's second style. I like its way to properly represent the indentation of function parameters AND its implementation, albeit that "unhappy face" is somewhat unusual to some other people.

Nowadays, PEP8 specifically gives an example for such case, so perhaps the mainstream is going to adapt that style:

    # Add 4 spaces (an extra level of indentation)
    # to distinguish arguments from the rest.
    def long_function_name(
            var_one, var_two, var_three,
            var_four):
        print(var_one)

We can of course go one step further, by separating each parameter into its own line, so that any future addition/deletion of parameter would give a clean git diff:

    # Add 4 spaces (an extra level of indentation)
    # to distinguish arguments from the rest.
    def long_function_name(  # NOTE: There should be NO parameter here
            var_one,
            var_two,
            var_three,
            var_four,  # NOTE: You can still have a comma at the last line
            ):  # NOTE: Here it aligns with other parameters, but you could align it with the "def"
        print(var_one)
RayLuo
  • 17,257
  • 6
  • 88
  • 73
12
def my_function(argument_one, argument_two, argument_three, 
                argument_four, argument_five):
kylieCatt
  • 10,672
  • 5
  • 43
  • 51
5

I personally like to line up the params one-per-line starting with the open parentheses and keeping that indent. flake8 seems happy with it too.

def guess_device_type(device_name: str,
                      username: str=app.config['KEY_TACACS_USER'],
                      password: str=app.config['KEY_TACACS_PASS'],
                      command: str='show version') -> str:
    """Get a device_type string for netmiko"""
Ben
  • 5,952
  • 4
  • 33
  • 44
  • There are a few technical problems with this style. 1) to parse a module full of functions, the eye has to constantly shift where it's scanning horizontally (unless all your function names are the same length ;) ). 2) Adding an argument after `command` will create spurious diff lines. 3) Entering the function block is a dedent (this is not intuitive since in python, indentation normally begins a block). – DylanYoung Jul 03 '20 at 18:57
  • 1
    @DylanYoung, Good points! These days, I use `black` to autoformat my code. – Ben Jul 04 '20 at 04:45
  • I would use Black in any new code as well. +1 Our issues are mostly around migrating old code, lol. Someday.... – DylanYoung Jul 07 '20 at 18:41
  • " the eye has to constantly shift where it's scanning horizontally" This is exactly the purpose of such formatting - distinguish the code between function parameters and local variables. – shs_sf Mar 19 '22 at 21:03
4

I find myself this way to be quite interesting:

def my_function(
        argument_one, argument_two, argument_three,
        argument_four, argument_five
):
    ...

it allows code-folding to reveal the function signatures quite easily, for instance, consider the below snippet:

def my_function(
        argument_one, argument_two, argument_three,
        argument_four, argument_five
):
    s1 = 1
    s2 = 2
    if s1 + s2:
        s3 = 3


def my_other_function(argument_one, argument_two, argument_three):
    s1 = 1
    s2 = 2
    if s1 + s2:
        s3 = 3

This way allows to code-folding the whole file and seeing all functions/signatures at once, ie:

enter image description here

BPL
  • 9,632
  • 9
  • 59
  • 117