Introduction

In the previous chapter, we got a comprehensive introduction to Python functions where we covered concepts like lexical scoping, closures, side effects, arguments and much more.

Arguments are an extremely vital concept related to functions, not just in Python, but in all programming languages that provide function out of the box. They are simply values we pass into a function which can then use them to tailor its process.

However, there are numerous ways to provide these values to functions and numerous ways to accept them from the perspective of functions themselves. All this shall be covered in this chapter.

Specifically, we'll see what are arguments and parameters in general; what are positional and keyword arguments in Python; how to provide default values to function parameters; what are variable — argument-length functions and how to create them using the *args and **kwargs parameter.

Let's begin!

What are arguments?

A function can take additional data from the user to carry out its respective task in a specific manner.

As the simplest example, consider a function sum() that aims to return the sum of two numbers a and b.

In order to do what it ought to, the function requires two numbers a and b and these are passed to it when it's called.

The names a and b are formally known as parameters of the function sum().

Parameters are defined in the header of a function's definition, within the pair of parentheses following the name of the function, as follows:

def func_name(parameters):
    # function body

When we call a function which has parameters defined, we have to specify values for those parameters (in most cases). For example, in the sum() function above, the moment we call it, we have to provide the two numbers whose sum we wish to know.

These values are provided inside the pair of parentheses in the function's invocation, and are known as arguments to the function.

An argument is a value provided to a function while calling it.

So, if we call sum(10, 20), 10 and 20 here are the arguments to sum().

People, generally, mix up the terms 'parameters' and 'arguments', using them interchangeably. Strictly speaking, this is incorrect.

A parameter refers to a name used in the pair of parentheses in a function's definition. An argument, on the other hand, refers to the value passed into the parameter.

Consider the following illustration:

Let's define the function sum() we've been discussing so far to see how all this theory looks and works.

A simple summing function

Restating its purpose, the function sum() takes in two numbers a and b and returns their sum.

The following code defines the function:

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

It simply states that there is a function sum() that takes in two arguments and returns back their sum.

Note that we don't know yet exactly what value do both these parameters a and b have; we have just defined what would happen when values are provided — they would be added together and the result returned.

Now let's call this function to sum up a few numbers:

sum(10, 1)
11
sum(-1, 1.5)
0.5
sum(2 ** 2, 10 ** 3)
1004

The values for a and b are provided in the same order that they both have in the function's definition.

In the definition, first comes the parameter a and then b; likewise the first argument to sum() would denote the value of a while the second argument would denote the value of b.

These types of arguments, that are put in parameters based on their positions in the invocation expression, are the most common, and known as positional arguments.

Remember that there is nothing in the definition of sums() that specifies that the arguments to a and b have to be numbers.

We provide numbers since we know that sums are usually associated with numbers — who would sum dictionaries or tuples, or Booleans anyway?

But we can call sum() and pass in non-numeric arguments as well.

Consider the snippet below:

sum('Hello ', 'World!')
Hello World!

Here we pass two strings to sum() and yet get something returned. As we know, what we get here is not the sum of the strings, but rather their concatenation.

The function receives two arguments and returns back the result of performing the + operator on them. It doesn't know at all as to what type of arguments they are!

We could also pass two entirely meaningless arguments to the function and still get it executed, but not necessarily to completion:

sum(True, {})
Traceback (most recent call last): File "<stdin>", line 4, in <module> File "<stdin>", line 2, in sum TypeError: unsupported operand type(s) for +: 'bool' and 'dict'

Here we pass a Boolean and a dictionary to sum() and sensibly get an error thrown — who told us that we could add Booleans to dictionaries!

This implies a very important thing, that's common to variables in Python as well (in fact, parameters are also variables, so the distinction is generally useless b/w parameters and variables).

And that important thing is that there is no way to fix the type of function parameters in Python. This is the reason why it's referred to as a dynamically-typed language — a variable can hold values of different types.

In some other languages that are based on a statically-typed model — such as C++, Java, etc. — a function parameter defines its type. Passing an argument of some other type results in an error.

Positional arguments

What we witnessed in the code above was an instance of positional arguments. Let's review what are positional arguments.

Arguments that match up parameters based on their position in the invocation expression are called positional arguments.

The idea is basic — the first argument is matched up by the first parameter, the second one is matched up by the second parameter, and so forth

Most of the argument that you'll work with in functions in Python will be positional. It's the simplest of all kinds of arguments, and so withnesses the largest amount of usage throughout Python.

Let's consider more examples using positional arguments.

Below we define a function to compute the distance between two pairs of co-ordinates on a Cartesian plane.

In each of the invocation from lines 6 to 9, we pass in positional arguments, to the function distance().

The parameters x1, y1, x2, and y2 as we shall see in the next section can be provided values using keyword arguments as well — another kind of function arguments. It's only if we explicitly specify that they must be positional that doing so would become the cause of an error. We'll see this in the last section.

Keyword arguments

Keyword arguments, as the name suggests, are specified with a keyword. The keyword is the name of a specific parameter for whom the corresponding value is specified.

Arguments that are provided as a name=value pair are known as keyword arguments.

A keyword argument looks exactly like a variable assignment expression — a name followed by an equals sign followed by the value binded to that name.

Let's consider a quick example. Below we have our same old sum() function with its two parameters a and b.

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

We'll call it to add the numbers 10 and 20, as before, but this time passing the values to the parameters using keyword arguments.

sum(a=10, b=20)
30

a = 10 is a keyword argument that assigns the value 10 to the parameter a while b = 20 is another keyword argument that assigns the value 20 to the parameter b.

Note that since keyword arguments directly bind a value to a given parameter, it doesn't matter whether one keyword argument comes before the other one. The order of keyword arguments simply doesn't matter...

This means that the invocation sum(b = 20, a = 10) is as good as the invocation sum(a = 10, b = 20).

sum(b=20, a=10)
30

In this case, it isn't invalid to call sum() and pass values to the parameters a and b via keyword arguments, but in most of Python's built-in functions, it is.

Even we could define our own function parameters in a way that restricts them to only accept positional arguments, or only keyword arguments. We'll see how to do this using the characters / and *, in the last section of this chapter.

Default values

Often times, when defining a parameter in a function, it's desirable to give it a default value, in case the user omits passing an argument for it.

For example, consider the code below:

def confirm(msg):
    inp = input(msg)
    return True if inp == 'y' else False

We have a function confirm() that shows the user a confirmation prompt, and returns back True if the user enters 'y' or else False.

Let's say that most of the times, we just want to show the message 'Confirm?'. Now in order to do so, with this definition in place, we would need to provide the message each time we call confirm().

def confirm(msg):
    inp = input(msg)
    return True if inp == 'y' else False

# some process
confirm('Confirm?')

# some other process
confirm('Confirm?')

If we don't want to display a specific prompt message, the msg parameter still needs to be provided the same string value, over and over again. This is not ideal!

A better way is to give the msg parameter a default value.

The general syntax of defining a default value for a parameter is shown below:

def func_name(param_name = default_value):
    # function's body

The parameter's name is followed by an equals sign which is followed by the default value. See how this resembles variable assignment and keyword argument syntax.

In our case, we need to set the string 'Confirm?' as the default value for the msg parameter in the function confirm().

This is accomplished below:

def confirm(msg='Confirm?'):
    inp = input(msg)
    return True if inp == 'y' else False

# some process
confirm()

# some other process
confirm()

See how the invocations in 6 and 9 don't provide any argument to the function. This is because, in both of them we desire to make a general prompt message asking the user for his/her confirmation — nothing specific is desired to be asked.

However, if a specific prompt message is desired, then the developer can pass in the respective string as the msg argument. In this case, the given argument would be used as the value for the msg parameter, instead of its default value.

confirm('Are you sure you want to exit? ')
Are you sure you want to exit? y

If you recall it, you'll see that many built-in functions and methods in Python have default-valued parameters.

Let's consider a few of these built-ins...

The print() function takes in a value and prints it to the REPL shell, ending it with a given character, or sequence of characters.

This ending can be provided via the end keyword argument. Be default, it has the value '/n' which denotes a newline character. That is, each print() statement ends with a new line i.e subsequent outputs in the shell each appear on a new line.

print('Hello')
print('World')
Hello
World

As stated, we can change this normal behaviour by explicitly providing a value to the end parameter, by means of a keyword argument.

print('Hello', end=' ')
print('World')
Hello World
We can't provide a value to the end parameter in the print() function, using a positional argument. This is because Python parses all positional arguments to print() as part of the output stream. end can only be provided a value via a keyword argument.

The sorted() function can be used to get back a sorted copy of a given iterable.

The iterable is required and provided to the function as a positional argument, after which we can additionally specify whether or not to reverse sort the iterable, using the reverse keyword argument.

If omitted, it defaults to False, which simply means that reverse-sorting (descending order) isn't desired.

sorted([11, 2, 9, 10, -5, -10])
[-10, -5, 2, 9, 10, 11]
sorted([11, 2, 9, 10, -5, -10], reverse=True)
[11, 10, 9, 2, -5, -10]

As another example, we have the int() function.

The int() function can be used to convert a numeric string into an integer value in Python. The string is provided as a positional argument, while an additional base description is given via the base argument.

base specifies the base of the numeric string, defaulting to 10 i.e the base of decimal numbers.

int('56')
56
int('1110', 2)
14

Other possible values are 2 for binary; 8 for octal; and 16 for hexadecimal numbers.

See Python Numbers — Base n Representation for more details on how to convert between different numeric bases in Python.

One crucially important thing to understand in regards to default-valued parameters is that the default values are computed only once, and that is when compiling the function.

Knowing this can prevent unexpected errors from happneing when you work with such functions.

Let's consider an example.

Given the code below, try to predict its output:

i = 5

def f(x = i):
    print(x)

i = 6
f()

Predict the output of the code above.

  • 5
  • 6

Did you choose 6?

Well, if you did, then you guessed the wrong output. The correct answer is 5!

5

But how does this even make sense — we have a global variable i, it has the value 5, we change it to 6 and then call the function f(), without an argument. This means that the parameter x gets the value i which we know is currently equal to 6.

Where does 5 come from?

Well, the scenario discussed here would've been absolutely correct had Python computed default values at runtime, when executing a function.

However, Python doesn't compute default values at runtime. Instead, this happens at compile time, when the function is compiled.

Going with this theory, let's see how the code above actually gets executed.

  1. A global variable i is created and assigned the value 5.
  2. The function f() is compiled, whereby the default value of its x parameter is evaluated. The value is i which resolves down to 5; likewise x points to the value 5.
  3. The global variable i is changed to 6.
  4. Next up, f() is called without any arguments. Thereby the default value of the parameter x is used which is 5. Recall that this value had been computed when the function was being compiled, at which point i was equal to 5.

This behaviour is similar to assigning a variable to another variable — both variables refer to the same value whereby reassigning to one does not obviously reassign the other.

But what if the latest value of i is desired in the case above?

Well, good question!

Just shift i from the parameter's default value to inside the function. Go with some placeholder value for the parameter, then on each call of the function, check if the parameter is equal to the placeholder value. If it is, then assign the value of i to the parameter.

This is illustrated in the code below:

i = 5

def f(x = None):
    if x == None:
        x = i
    print(x)

i = 6
f()
6

The reason this works is simply because a function's body is always executed at runtime.

*args parameter

In Python, it's possible to provide a variable number of arguments to a function. One way to make a function like this is demonstrated above — set default-valued parameters.

Given these parameters, we can make function calls either by providing the respective argument or not providing it. This clearly shows that it's possible to call the given function with a varying number of arguments.

There is yet another way to call functions with a variable length of arguments, in fact two such ways: one is using the *args parameter and the other is using the **kwargs parameter.

Let's start discussing on the former.

Suppose we want to create a sum() function that could take in an arbitrary number of arguments and then return back the sum of all of them.

What we want is something like the following:

sum(10, 5, 6, -1)
20
sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
55

This could simply be achieved if we use a parameter in the definition of sum() of the form *args where arg is the name of the parameter.

*args is special kind of a parameter that can accept multiple positional arguments, and then provide them encapsulated in a tuple.

Here's how to use *args:

def func_name(*args):
    # function body

It can only come after normal parameters of a given function. When it's specified, all the arguments, written after those belonging to normal parameters of the function, are passed to the *args parameter in the form of a tuple.

Each individual argument, is then obviously, accessible by means of bracket notation on the parameter (we just have to retrieve individual elements from a tuple).

Let's consider a simple example:

def f(*args):
    print(args)

A very simple function f() is defined here with a single *args-type parameter, itself named *args, whose value is printed.

Now, let's call f() with a bunch of different arguments and see what do we get in the args parameter:

f(1, 5, 10)
(1, 5, 10)
f(True, False)
(True, False)
f()
()

As is clear, whatever we send into f(), it's output back to us, but in the form of a tuple.

Shown below is another example:

def f(x, *args):
    print('x:', x)
    print('args:', args)

Here the function f() also has an *args-type parameter, once again named args, but this time it comes after a normal parameter i.e x.

Let's see how args receives values in this case:

f(1, 5, 10)
x: 1
args: (5, 10,)
f(True, False)
x: True
args: (False,)
f()
Traceback (most recent call last): File "<stdin>", line 1, in <module> f() TypeError: f() missing 1 required positional argument: 'x'

In each invocation of f(), the first argument acts as a positional argument for the parameter x. Then the rest of the arguments go in the *args parameter, in the form of a tuple.

The third statement above throws an error, since no argument is provided for the parameter x.

Coming back to our sum() function, here's how we can use an *args-type parameter to make it sum any number of numbers:

def sum(*nums):
    total = 0

    for num in nums:
        total += num

    return total

Here the *nums parameter is of the form *args. It simply means that all arguments provided to sum() starting from the first argument would be put in a tuple and that tuple passed to the nums parameter.

In the function, we iterate over all elements of the nums tuple and add them to the total local variable, finally returning it.

Now if we call something like sum(10, 20, 30) here's what we'll get:

sum(10, 20, 30)
60

Amazing!

The best part is that it is perfectly valid to use other parameters in a function in addition to *args.

Below we define a function power_sum() that takes in a given power and then sums all the provided numbers raised to that power.

def power_sum(pow = 1, *nums):
    total = 0

    for num in nums:
        total += num ** pow
    
    return total

We'd call this function as follows:

power_sum(2, 3, 4)
25

So what's happening here?

power_sum(2, 3, 4) means adding the numbers 3 ** 2 and 4 ** 2. The first argument i.e 2, goes inside the pow parameter, while the rest of them go inside the nums parameter.

In the loop, each number in nums is raised by pow and then added to the total. This total is finally returned by the function.

This is a good example to demonstrate that it's possible to use normal parameters along with *args. In fact, it's very common to do so along with *args-type parameters.

It's quite occasional to see functions with only an *args parameter — processing an arbitrary number of arguments, without other configurative arguments, is very rare.

For example, consider the print() function. It can accept an arbitrary number of arguments and then print them to the shell one after another.

print(10, 20, 30)
10 20 30
print(True, 'Hello', 10, -1, 36.5)
True Hello 10 -1 36.5

However, it can also be passed other configurative arguments such as sep, end etc. to further tailor the output:

print(10, 20, 30, sep=', ')
10, 20, 30
print(10, 20, 30, end='...')
10 20 30...

Things to note when using *args

There are a couple of things one should know before working with the *args parameter, that could otherwise lead to unexpected errors and poor function layouts, if not known.

They are detailed as follows:

Parameters after *args are keyword-only

While defining a function, if a parameter comes after the *args parameter, it is considered to be keyword only.

Any parameter after *args can only accept keyword arguments.

This is because any number of positional arguments in the invocation expression would go in the *args parameter, leaving nothing for the one following *args. That parameter can only

The power_sum() function we created above, could be defined in a better way, using this idea. Let's first see what is the problem with it right now.

When we call power_sum(2, 3, 4), ideally we should get 9 returned. That's because, on the first sight, the expression looks as if it's summing the numbers 2, 3 and 4.

power_sum(2, 3, 4) # should ideally return 9
25

However, as we know, the first argument is not part of the sum; instead, it represents the power to which each number in the sum is raised.

The thing is that, the way our function has been laid out, it's not really easy to understand what's happening in a given invocation. A reader might not be able to make the right intuition regarding the result of power_sum(2, 3, 4).

The function works absolutely fine for the computer — it's just for the reader that the function isn't able to convey its meaning in a simple way.

What we could do to power_sum(), to make it more expressive when called, is to change the position of the pow parameter.

Consider the code below:

def power_sum(*nums, pow=1):
    total = 0

    for num in nums:
        total += num ** pow
    
    return total

We've moved pow after the *nums parameter. This means that pow can only accept keyword arguments, NOT positional arguments. If we don't specify any keyword argument, pow would obviously default to 1.

Now if we need to sum the numbers 3 ** 2 and 4 ** 2 (each raised to the power 2), we would call:

power_sum(3, 4, pow=2)
25

Moreover, our previous expression power_sum(2, 3, 4) would now return 9. This is because there is no keyword argument specified for pow, and so it would default to 1 i.e add the numbers as they are:

power_sum(2, 3, 4)
9

As you would agree, power_sum() is now much more intuitive. Both the given invocations are a lot more meaningful.

For instance, a reader might be able to reason, just by reading, that power_sum(3, 4, pow=2) sums the squares of the numbers 3 and 4 when he/she sees the output 25.

In contrast, reading the expression power_sum(2, 3, 4) and then the output 25, a reader might not be able to quickly reason the output, or reason it at all!

Laying out functions is surely an art!

Only one *args parameter is allowed

One more thing — it is invalid to have two *args parameters in a function. What's the reason?

Think on it for a while — two *args parameters essentially mean the following: 'the rest of the arguments go in the first *args-type parameter, and then the rest of the arguments go in the second *args-type parameter.'

Do you think this sounds sensible?

Below shown is an illustration:

def f(*a , *b):
    pass
SyntaxError: invalid syntax

The syntax error highlights the asterisk before the parameter b, trying to tell that it's invalid to have more than one parameter with it i.e it's invalid to have more than one *args-type parameter.

To boil it down, a function can have at most one *args parameter. That's it!

Below shown is an example:

def f(x, *args):
    print(x, args)

f(30, 20, 5, x=10)

We might expect that this would work as follows: the keyword argument x=10 assigns a value to the x parameter, and so the args 30, 20 and 5 go in the *args parameter.

But even in saying this out, we don't make any sense, at all!

Python parses code from left to right, which is a trivial detail. It doesn't see the way we might see code.

First 30 is matched up with the first parameter in the function's definition which is x, then the rest of the arguments (before the keyword argument) are put in the *args parameter. Then the keyword argument x=10 assigns a value to the parameter x, which already has a value.

Passing two values to a parameter in Python is invalid; hence this throws an error.

**kwargs

The second kind of parameter that makes a function accept a variable number of arguments is **kwargs.

**kwargs is a special kind of a parameter than can accept multiple keyword arguments, and then provide them encapsulated in a dictionary.

The difference between *args and **kwargs is that the latter receives the rest of the keyword arguments as a dictionary, whereas the former receives the rest of the positional arguments as a tuple.

This is where **kwargs gets its name from — 'kwargs' stands for 'keyword arguments'.

Each item in the kwargs dictionary holds one of the given keyword arguments. The item's key represents the name of the argument, while its value represents the value of the argument.

Here's its general form:

def func_name(**kwargs):
    # function's body

As with *args, **kwargs denotes the general form of this type of parameter — kwargs here is the name of the parameter; it's not necessary to use the name kwargs.

Let's see **kwargs in action.

def f(**items):
    print(items)
f(x=10, y=20, z=30)
{'x': 10, 'y': 20, 'z': 30}
f()
{}

As can be seen above, when no arguments are passed to f(), the items parameter is an empty dictionary.

Things to note when using **kwargs

As with *args, there are a couple of things to make sure when working with the **kwargs parameter. Those are discussed as follows...

**kwargs must be the last parameter

When a function is defined with a **kwargs parameter, no other parameter shall come after it. The **kwargs parameter must be the last one in the sequence of parameters.

Can you reason why is this so?

A **kwargs parameter digests all keyword arguments specified in a function's invocation expression. This means that following **kwargs there can be no keyword arguments for other parameters — they will end up in **kwargs instead of those respective parameters.

However, following **kwargs it could've been possible to allow positional arguments, but this would've come at the cost of ambiguity. As one instance, consider the following code:

def (*nums, **items, x):
    print(x)

First of all note that this is not valid Python code — it's just written to show you why might've the Python community not allowed having any parameter after **kwargs.

Suppose that you want to omit specifying any value to **items here. You would write something as follows:

f(10, 20, 30, 40) # 40 is meant for x

See the problem with this. The *nums parameter before **items, digests all positional arguments; likewise the argument meant for the parameter x goes into nums. This would've caused an error.

Parameters after **kwargs could've potentially caused confusion, both for the Python interpreter and for the human reader.

By disallowing parameters to come after **kwargs, Python eliminates any sort of ambiguity that could've otherwise crept into programs. It normalises parameter definitions, and makes them look more sensible and pleasing.

For instance, *args has one asterisk in it, while **kwargs has two of them; so naturally our mind would like to have the latter afterwards (in the function's definition).

Summarising this long discussion in a few words, Python doesn't allow having any argument after **kwargs for very good reasons!

Only one **kwargs parameter is allowed

Aking to *args, a function can have at most only one **kwargs parameter.

The reason is similar to that of allowing only one *args parameter.

Consider the code below:

def f(**a, **b):
 
SyntaxError: invalid syntax

As expected, the statement returns a syntax error.

In the code above, the error is caused because there is some parameter following the first parameter **aNOT because there is a **kwargs-type parameter following it.

Recall the first point above — it's invalid to have any parameter after **kwargs. This is what's happening here. Python realises that a parameter follows the first **a parameter and likewise throws an error.

This example is meant to demonstrate that when we say no parameter can follow **kwargs, it includes another **kwargs parameter as well.

The / and * symbols in parameters

Have you ever seen the documentation of a built-in function when writing it in the REPL shell, or when exploring it in Python's docs online?

Often, you'll see the symbols / and * used between parameters.

These symbols can be used while defining a function to distinguish between parameters that could accept only positional arguments only, keyword arguments only, or both.

Consider the snippet below:

def func_name(pos_only, /, pos_or_keyword, *, keyword_only):
    # function body

It specifies that all parameters before / can accept only positional arguments, all parameters after * can accept only keyword arguments, and everything in between them can accept both.

Let's recreate the confirm() function, we constructed above, to accept only a positional argument in the msg parameter:

def confirm(msg, /):
    inp = input(msg)

    if inp == 'y':
        return True
    return False

If we now call confirm(msg='Exit? '), we would get back an error:

confirm(msg='Exit? ')
Traceback (most recent call last): File "stdin", line 1, in <module> confirm(msg='Exit?') TypeError: confirm() got some positional-only arguments passed as keyword arguments: 'msg'

This is because the / (forward slash) character clearly implies that msg can only be provided a value via a positional argument.

It's also possible to make msg keyword-only:

def confirm(*, msg):
    inp = input(msg)

    if inp == 'y':
        return True
    return False

Now, if we call confirm('Exit? '), we would get an error, as msg is now obliged to be supplied a value via a keyword argument only, with the help of the * (asterisk) character.

confirm('Exit? ')
Traceback (most recent call last): File "stdin", line 1, in <module> confirm(msg='Exit?') TypeError: confirm() got some keyword-only arguments passed as positional arguments: 'msg'

Let's create another function that applies both restrictions on given parameters:

def confirm(msg, /, *, true_input):
    inp = input(msg)

    if inp == true_input:
        return True
    return False

The previous function confirm() has been modified. We can additionally specify a value in the true_input parameter to be used in the check returning True, instead of the default value 'y'.

Below a couple of calls of this function are made:

confirm('Exit? ', true_input='1')
Exit? 1
True

As we know, without these symbols, function parameters have no restrictions at all. They can be supplied values via both, positional and keyword arguments.