Function Parameters in Python
6 min read

Function Parameters in Python

Function Parameters in Python

This post assumes familiarity with the concept of a function, and how they work. It's not necessary to know exactly how they work in Python, but if you don't understand functions, you should go and read up on them first.

Arguments

Let's take a look at a very simple python function, one you've almost certainly seen in almost every single tutorial ever.

def add(a, b):
    return a + b

This takes 2 numbers as input (a and b), adds them together and returns the result. It's predictable, you can see what you need to pass in, and are pretty sure you could calculate the result in advance.

a and b in this function are called arguments.

Specifying them in this way means they are both mandatory. Calling add with only one parameter will give an error.

TypeError: add() missing 1 required positional argument: 'b'

Calling add with too many arguments will give a different type of error.

TypeError: add() takes 2 positional arguments but 3 were given

So what if we wanted to add either 2 or 3 numbers? Would we need to define 2 functions?

def add2num(a, b): ...
def add3num(a, b, c): ...

I'm sure you can guess already that the answer is "of course not". There are actually 2 ways we could solve this problem.

Optional arguments

We can add a third argument in as optional and set a default value that will not affect the final result.

In [1]: def add(a, b, c=0):
   ...:     return a + b + c

In [2]: add(1, 2)
Out[2]: 3

In [3]: add(1, 2, 3)
Out[4]: 6

When we pass in only 2 values(a and b), the function still receives 3 values (a, b and c), but uses the default value of 0 for c that we specify in the function definition, which of course is the identidy property for addition.

An identity property is a value which, when applied to a mathematical operation, leaves the original value the same. For addition, the identity is 0 becuase a + 0 = a. For multiplication is is 1 becuase a * 1 = a.

a and b are still mandatory values, so this function requires either 2 or 3 values to be passed to it, or it will throw one of the errors above.

The next way of solving the problem of an optional 3rd parameter is a little more flexible.

Using *args

It is also possible to specify an indefinite number of arguments to a function.

In [1]: def add(*args):
   ...:     return sum(args)

In [2]: add(1)
Out[2]: 1

In [3]: add(1, 2)
Out[3]: 3

In [4]: add(1, 2, 3)
Out[4]: 6

In [5]: add(1, 2, 3, 4, 5)
Out[5]: 15

Ignore for the moment the fact that all this function really does is effectivly add an alias called add to the built-in Python sum - we're interested in how the arguments work, not the contents of the function itself. The function could equally have run a for loop to add the numbers up

Adding a print statement into the function, we can see that the value of args is actually a tuple, which is why we can call sum on it.

In [6]: def add(*args):
   ...:     print(args)
   ...:     return sum(args)

In [7]: add(1, 2, 3, 4)
(1, 2, 3, 4)
Out[7]: 10

Keyword arguments

We've seen a keyword argument already, when we set an optional argument for c=0 above. In this case, the keyword was c and the default value was 0.

Keyword arguments need to come after all the "regular" arguments, so in the example above attempting to define b as the optional parameter would not have worked.

In [1]: def add(a, b=0, c):
    ...:     return a + b + c
  File "<ipython-input-25-aa8377392da9>", line 1
    def add(a, b=0, c):
            ^
SyntaxError: non-default argument follows default argument

Positional-only arguments

When we call our orignal add(a, b) function it didn't really matter whih variable was a and which was b, since addition is commutative

In maths, a commutative operation is one where the order of the elements does not affect the result. Addition is commutative because a + b = b + a, but subtraction is not because a - b != b - a (at least, not usually).

Returning to the original function, we are allowed to specify the variable names when calling the function, rather than relying on the order.

In [1]: def add(a, b):
   ...:     return a + b
   ...:

In [2]: add(1, 3)
Out[2]: 4

In [3]: add(b=3, a=1)
Out[3]: 4

So as you can see, add(1, 3) is exactly the same as add(b=3, a=1), even down to the fact that a==1 and b==3 in both cases - in the first case because the ordering dictates it, and in the second because we have overridden the ordering to specify the variable names and their values.

Let's extend the function a little so that the order becomes a little more important.

In [4]: def add(a, b, opr='add'):
    ...:     if opr == 'sub':
    ...:         return a - b
    ...:     return a + b
    ...:

In [5]: add(1, 3)
Out[5]: 4

In [6]: add(1, 3, 'sub')
Out[6]: -2

Not specifying the opr parameter defaults it to add, but we can override the operation by setting it to sub instead.

Note that once you have set an optional parameter like this in the form key=val, you cannot follow it by a positional parameter.

In [7]: add(1, 3, 'sub', False, True)
Out[7]: -2

In [8]: add(1, 3, 'sub', do_even_more=False, do_more=True)
Out[8]: -2

When calling the function, swapping the order of a, b and opr would cause issues here if you didn't specify the variable names.

In [9]: add(1, 'sub', 3)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-38-1434c44c86cc> in <module>
----> 1 add(1, 'sub', 3)

<ipython-input-35-4f508039cc33> in add(a, b, opr)
      2     if opr == 'sub':
      3         return a - b
----> 4     return a + b
      5

TypeError: unsupported operand type(s) for +: 'int' and 'str'

Not entirely surpising, since sub is a string and so we can't add it to a number! We could do this ...

In [10]: add(1, opr='sub', b=3)
Out[10]: -2

But frankly it's all starting to look a little confusing now. Wouldn't it be simpler if we just said all these parameters were positional only? It would certainly make documenting the function simpler, and avoid confusion for anybody wanting to call it.

Remember the Zen of Python says There should be one-- and preferably only one --obvious way to do it.

Python allows us to specify positional only parameters by simply putting a slash (/) after the last positional only parameter.

In [11]: def add(a, b, opr='add', /):
   ...:     if opr == 'sub':
   ...:         return a - b
   ...:     return a + b

In [12]: add(1, 3, opr='sub')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-10-aec68a30c86f> in <module>
----> 1 add(1, 3, opr='sub')

TypeError: add() got some positional-only arguments passed as keyword arguments: 'opr'

Here, you can see that just specifying the opr= in the call raises an error, even though the parameters are all specified in the correct order. The extra slash enforces this.

The slash doesn't need to be the end of the parameter list, but anything that comes after it can be set using key=val as well as position.

def add(a, b, opr='add', /, do_more=False, do_even_more=True):
    if do_more:
        pass # do other stuff here
    if do_even_more:
        pass
    if opr == 'sub':
        return a - b
    return a + b

To set do_more and do_even_more we could set them positionally - in order - as the final arguments, or specify the parameter names.

In [13]: add(1, 3, 'sub', False, True)
Out[13]: -2

In [14]: add(1, 3, 'sub', do_even_more=False, do_more=True)
Out[14]: -2

Both of these calls are exactly the same, but because of the slash, both require the first 3 paramters to be set based on their position only.

Keyword-only arguments

In much the same way as using the slash to set the end of the positional-only argument list, we can also use an asterix (*) to set the beginning of the keyword-only parameter list. This can be combined with the slash value to effectively split the parameter list into 3 parts:

def function_name(<positional only>, /, <positional or keyword>, *, <keyword only>)
eg.
def function_name(a, b, c, /, d=0, e=0, *, f=0):

In this example, a, b, and c, must be positional, d and e could be positional or specifid using their names, and f can only be specifid by f=value.

Let's return to the earlier function and add one more parameter.

def add(a, b, opr='add', /, do_more=False, do_even_more=True, *, do_yet_more=False):
    if do_more:
        pass # do other stuff here
    if do_even_more or do_yet_more:
        pass
    if opr == 'sub':
        return a - b
    return a + b

Here, a, b and opr are positional only, do_more and do_even_more could be specified by their position or name, and do_yet_more must be set by name.

The following calls would all be valid:

add(1, 2, 'sub', True, do_even_more=False)
add(1, 2, 'sub', True, do_even_more=False, do_yet_more=True)
add(1, 2)

This call would raise an error:

add(1, 2, 'sub', True, False, False)

Watch out for ...

If you omitted opr (which is OK becuase it has a default value of add) and then try to set do_more by position, i.e add(1, 2, True), no error will be raised, but becuase you are setting the values positionally, True will feed into opr, which could have unintended consequences for your process.

Enjoying these posts? Subscribe for more