Sitemap
Vašek Pelc

Vašek Pelc

Parameters in Python functions and their order

8 min readMay 19, 2023

--

Have you ever seen a definition of a function in Python with a mixture of names, asterisks, sometimes sprinkled with a single forward slash, and wondered what it all means? Well, wonder no more.

The basics

There are two ways of passing arguments to functions in Python:

  1. by position,
  2. by name.

Imagine the following function definition:

def add_two_numbers(first: float, second: float) -> float:
print(f'The output is: {first + second}')

This function can be called by a number of ways which are equal and should always print the message The output is: 3:

# Call with arguments passed by position only
add_two_numbers(1, 2)

# Two equivalent calls with arguments passed using keywords only
add_two_numbers(first=1, second=2)
add_two_numbers(second=2, first=1)

# Call with mixed use of position and keywords
add_two_numbers(1, second=2)

You may wonder why there isn’t a case where the keyword is first and the positional argument second, i.e.,

add_two_numbers(first=1, 2)

That’s because there is a rule in Python where positional arguments always have to go before keyword arguments, otherwise you get the message SyntaxError: positional argument follows keyword argument.

Parameters in functions can also have default values which are used in case the user doesn’t provide them, making the parameters optional:

def add_two_numbers(first: float, second: float = 0) -> float:
print(f'The output is: {first + second}')

With this, the user doesn’t have to provide the second argument to our function and can simply call

add_two_numbers(1)

which should print The output is: 1.

One important thing to notice here is the order of parameters in the function definition. The order has to be

  1. parameters without default values,
  2. parameters with default values.

If the definition of our function looked like

def add_two_numbers(first: float = 0, second: float) -> float:
print(f'The output is: {first + second}')

we would get an error saying SyntaxError: non-default argument follows default argument. There is a case where the above order doesn’t apply that strictly and we get to it later.

Specifying positional-only arguments

With the basics covered, let’s look at another situation. What if you have a function and you want to tell it that some parameters can be called by keywords while others need to be positional? This is where the forward slash / comes to play. For example, in the following function

def add_three_numbers(first: float, /, second: float, third: float) -> float:
print(f'The output is: {first + second + third}')

we are telling the function that the argument first is positional-only. We can do the following:

# Call with all arguments positional
add_three_numbers(1, 2, 3)

# Call with two positional and one keyword arguments
add_three_numbers(1, 2, third=3)

# Calls with one positional and two keyword arguments
add_three_numbers(1, second=2, third=3)
add_three_numbers(1, third=3, second=2)

However, if we try to pass the first argument to the function using its keyword like this

add_three_numbers(first=1, second=2, third=3)

we get the error TypeError: add_three_numbers() got some positional-only arguments passed as keyword arguments: 'first'.

What about default values? Well, the order has to be always the same

  1. parameters without default values,
  2. parameters with default values.

If you have a parameter with a default value before the forward slash, you have to assign default values to all positional arguments after the forward slash. In case all the parameters before the forward slash are mandatory, you may have mandatory parameters that can be invoked by position or their keyword after the forward slash as well, followed by optional parameters with default values.

How does the asterisk fit in?

We’ve learned about the positional arguments and how to make them positional-only. How about the asterisk? There are multiple use cases for the asterisk.

Using asterisk as ‘catch-all’ for positional arguments

The asterisk can be used with a variable name as a ‘catch-all’ parameter for positional arguments. It can either stand alone

def add_numbers(*numbers) -> float:
sum = 0
for number in numbers:
sum += number
print(f'The sum is {sum}')

in which case we can call the function with various number of arguments

# The sum is 0
add_numbers()

# The sum is 1
add_numbers(1)

# The sum is 3
add_numbers(1, 2)

# The sum is 6
add_numbers(1, 2, 3)

You can also unpack a list, a tuple or some other iterable using

# The sum is 15
list = [1, 2, 3, 4, 5]
add_numbers(*list)

You can also reference various positions in the numbers parameter inside of the function like numbers[1], numbers[2] but you may run into troubles because the index doesn’t have to be there necessarily. If you need some positional arguments to always be there, it is better to use the asterisk parameter in the following way:

def add_numbers(first: float, second: float = 0, *numbers) -> float:
sum = first + second
for number in numbers:
sum += number
print(f'The sum is {sum}')

Now, the first argument is optional, the second argument is optional but always defined, and the rest is optional and does not have to be defined. You can call the function like this:

# The sum is 1
add_numbers(1)

# The sum is 3
add_numbers(1, 2)

# The sum is 6
add_numbers(1, 2, 3)

# The sum is 15
list = [3, 4, 5]
add_numbers(1, 2, *list)

However, calling

add_numbers()

raises the error TypeError: add_numbers() missing 1 required positional argument: 'first'.

Get Vašek Pelc’s stories in your inbox

Join Medium for free to get updates from this writer.

There are some things you need to take into account when working with ‘catch-all’ parameters:

  • you cannot actually assign the ‘catch-all’ argument by any keyword (you cannot do add_numbers(1, 2, numbers=list),
  • if you use an assignment by keyword for any previous arguments, you won’t be able to assign it at all (you cannot do add_numbers(1, second=2, 3, 4) or add_numbers(1, second=2, numbers=list) ),
  • since its position is always at the end of the list, you cannot skip the optional positional arguments before assigning it,
  • you cannot have any positional arguments after the ‘catch-all’ parameter.

Defining keyword-only arguments

With the last point, we are getting to an interesting concept: Some parameters are keyword-only. This can be used, e.g., if you want to make sure that the user of your function touches the parameter only if they really know what they are doing (they have to type its name to set it). The keyword-only arguments always have to be positioned after the ‘catch-all’ variable:

def do_two_numbers_operation(first: float, second: float, *args, operation: str) -> float:
if operation == 'add':
return first + second
if operation == 'multiply':
return first * second

This tells the function that the operation parameter is keyword-only. You can call it like this

# Returns 3
do_two_numbers_operation(1, 2, operation='add')

# Returns 2
do_two_numbers_operation(1, 2, operation='multiply')

You can also call it with many more positional arguments and it will still work:

# Returns 3
do_two_numbers_operation(1, 2, 3, 4, 5, 6, operation='add')

# Returns 2
do_two_numbers_operation(1, 2, 3, 4, 5, 6, operation='multiply')

Without the keyword argument, this would raise an error TypeError: do_two_numbers_operation() missing 1 required keyword-only argument: 'operation'. To make the argument optional, we just need to assign it a default value:

def do_two_numbers_operation(first: float, second: float, *args, operation: str = 'add') -> float:
if operation == 'add':
return first + second
if operation == 'multiply':
return first * second

Now, if we call it without the operation argument, it will do the addition:

# Returns 3
do_two_numbers_operation(1, 2, operation='add')

# Returns 2
do_two_numbers_operation(1, 2, operation='multiply')

# Returns 3
do_two_numbers_operation(1, 2)

# Returns 3
do_two_numbers_operation(1, 2, 3, 4, 5, 6)

But what if you don’t want to catch all the positional arguments and still have keyword-only arguments? You can use the asterisk without any parameter name

def do_two_numbers_operation(first: float, second: float, *, operation: str = 'add') -> float:
if operation == 'add':
return first + second
if operation == 'multiply':
return first * second

If you call the function with more than two positional arguments, you get the error TypeError: do_two_numbers_operation() takes 2 positional arguments but 3 were given . Of course, the number given will vary based on how much you actually provide. Anyway, the rest should work in the same way as the previous code

# Returns 3
do_two_numbers_operation(1, 2, operation='add')

# Returns 2
do_two_numbers_operation(1, 2, operation='multiply')

# Returns 3
do_two_numbers_operation(1, 2)

Using two asterisks as ‘catch-all’ for keyword arguments

You have probably encountered code where there were two asterisks in the parameter definition of a function. Two asterisk in front of a parameter name is a ‘catch-all’ for keyword arguments.

Let’s use the function do_two_numbers_operation that we defined above. If we try to run it like this

do_two_numbers_operation(1, 2, operation='multiply', some_other_keyword='some_value')

We get the following error TypeError: do_two_numbers_operation() got an unexpected keyword argument 'some_other_keyword'. To prevent such behavior, we can catch all the keyword arguments that are not explicitly defined by defining the ‘catch-all’ argument with double asterisk in front of it in the following way:

def do_number_operation(first: float,
second: float,
*other_numbers,
operation: str = 'add',
number_multiplicator: float = 1,
**other_keywords) -> float:
if operation == 'add':
sum = number_multiplicator * (first + second)
for number in other_numbers:
sum += number_multiplicator * number
return sum
if operation == 'multiply':
sum = first * second * number_multiplicator**2
for number in other_numbers:
sum *= number * number_multiplicator
return sum

With this code, there is really a lot of ways to run the function, for example:

# Returns 3
do_number_operations(1, 2)

# Returns 6
do_number_operations(1, 2, 3)

# Returns 2
do_number_operations(1, 2, operation='multiply')

# Returns 6
do_number_operations(1, 2, number_multiplicator=2)

# Returns 12
do_number_operations(1, 2, 3, number_multiplicator=2)

# Returns 48
do_number_operations(1, 2, 3,
operation = 'multiply',
number_multiplicator=2)

# Returns 48
do_number_operations(1, 2, 3,
operation = 'multiply',
number_multiplicator=2,
some_other_keyword_argument='some_value')

Notice that the last one has a keyword parameter some_other_keyword_argument that isn’t explicitly in the definition. However, it doesn’t matter because it is processed by the **other_keywords part. How do we access keywords from the keyword ‘catch-all’ argument? We can simply work with it as we would with a dictionary

from typing import Optional

def do_number_operation(first: float,
second: float,
*other_numbers,
operation: str = 'add',
number_multiplicator: float = 1,
**other_keywords) -> Optional[float]:
if other_keywords.get('skip_this_function')
print('Skipped the function')
return
if operation == 'add':
sum = number_multiplicator * (first + second)
for number in other_numbers:
sum += number_multiplicator * number
return sum
if operation == 'multiply':
sum = first * second * number_multiplicator**2
for number in other_numbers:
sum *= number * number_multiplicator
return sum

Now, if we run the following code:

do_number_operations(1, 2, 3,
operation = 'multiply',
number_multiplicator=2,
skip_this_function=True)

We get the message Skipped the function and the function returns None.

Note that unlike a single asterisk, we cannot have the double asterisk alone, there always have to be a variable it’s tied to, otherwise we get a SyntaxError.

Closing words

Now you should know what asterisks, double asterisks, and forward slashes are for in function definitions so next time you see them, you won’t be confused by them. Just to make sure, in what ways can we call this function so it’s valid and we don’ get any errors?

def my_function(one: float, two: float, /,
three: float, four: float = 4, *args,
five: float, six: float = 6, **kwargs) -> float:
pass

TL;DR

A forward slash in Python function definitions separates positional-only arguments and arguments that can be both positional and keyword arguments (positional_only, /, both).

A single asterisk separates arguments that can be both positional and keyword, and keyword-only arguments (this_is_both, *, keyword_only). If we use it together with a variable (this_is_both, *args, keyword_only), we can access the variable as a list (args[0]).

A double asterisk works as a ‘catch-all’ for keyword arguments, is always at the end of arguments definition, and has to be tied to a variable (variables, **kwargs). We can access the variable as a dictionary ( kwargs['some_additional_keyword_argument], kwargs.get('some_other_keywords_argument')).

--

--