Intermediate: Flexible Inputs and Outputs

Table of Contents

Parameters and Arguments

In this week’s beginner notebook, we introduced functions and described the values they take in and give out as inputs and outputs. In this notebook, we will use some more precise Python terminology for inputs: parameters and arguments.

  • A parameter is the variable listed in a function’s definition. It acts as a placeholder that says “this function expects a value here.”
  • An argument is the actual value you provide when calling the function.

For instance, consider the simple function below:

In this case, name is the parameter. It is the label inside the function that will hold the input. "Sofia" is the argument. It is the concrete value we supply when calling the function.

There are two main ways to pass arguments to a function in Python: - Positional arguments are matched to parameters by the order they appear. - Keyword arguments are matched to parameters by name, using the parameter=value syntax.

To see what we mean here, consider the following example:

The first call to make_greeting uses positional arguments. This means that arguments are assigned based on the order they are passed into the function. Here, "Hello" is matched to greeting and "Brad" is matched to name by position.

The second call to make_greeting uses keyword arguments. This means that the arguments are matched by name, using the parameter=value syntax. Here, "Liam" goes to name and "Hi" goes to greeting, even though the order is reversed.

In the first half of this notebook, we will look at the different kinds of parameters Python allows us to define, and how arguments can be passed to them in flexible ways.

Arbitrary Positional and Keyword Arguments

Sometimes when coding you may want a function to allow an arbitrary number of inputs. This can be useful because it reduces repetition and makes your code cleaner.

For example, consider the following repetitive code:

This is not very efficient. It would be much easier if we could just write one function that sums any number of values. One way to do this is to pass a tuple:

This works, but notice that the function calls require two sets of round brackets, which, even for this simple example, is slightly clunky and could easily lead to mistakes.

Python provides a cleaner way of writing the above code using the * operator. This collects all positional arguments into a single tuple inside the function like so:

The code inside the function is essentially the same, but now the function calls look much cleaner since we can pass in values directly without square brackets.

By convention, when using the * operator to pass arbitrary positional arguments into a function, we name the tuple we pass in args, which stands for “arguments”.

So far we have seen how to accept an arbitrary number of positional arguments. But what if we want to accept an arbitrary number of keyword arguments?

Imagine we tried to write separate functions for describing a person:

This code is pretty repetitive and messy. One way we could imagine simplifying this code is by using if statements and a dictionary, like so:

This works, but again the code feel awkward because we now have to write out a dictionary whenever we call the function.

Python provides a cleaner way of writing the above code the ** operator. This gathers all keyword arguments into a dictionary inside the function:

Instead of naming the dictionary my_dict like in the above code, by convention we use tend to use the name kwargs, which is short for “keyword arguments”.

In the same way that we can write a function which takes as input both a tuple and a dictionary, such as below:

We can instead write a function that takes both arbitrary positional arguments and arbitrary keyword arguments, using *args and **kwargs:

Positional-Only and Keyword-Only Arguments

So far, we have seen that function arguments in Python can normally be passed either by position or by keyword:

Both calls work the same way. But sometimes we want more control. Python gives us two special symbols for this:

  • / means everything to the left of it must be positional-only.
  • * means everything to the right of it must be keyword-only.

So you can think of / and * as markers in the parameter list that divide it into three possible regions:

my_function(a,b,/,c,d,*,e,f)

Here, we have that:

  • a and b are positional-only parameters.
  • c and d can be specified using position or keyword.
  • e and f are keyword-only parameters.

Let’s, look at some examples. The below function will take positional arguments only:

Here, x and y are to the left of /, so they can only be given by position.

Similarly, this example will accept keyword arguments only:

Here, name and age are to the right of *, so they must be given as keywords.

We can allow a function to have a mix of all three types of parameters (positional-only, keyword or positional, or keyword-only) as follows:

Test your Understanding: Without writing code, consider the following expressions:

  • demo(1, 2, c=3, d=4, e=5, f=6)
  • demo(a=1, 2, 5, e=3, d=4, f=6)
  • demo(1, 2, c=5, e=3, d=4, f=6)
  • demo(1, 2, d=3, 4, e=5, f=6)

What do you think each of the above will print and why? Will any of them error? If so, which?

Multiple Returns

So far, we’ve spent time looking at how you can flexibly specify arguments for functions. Now let’s shift focus slightly and look at another useful trick when writing functions: multiple return statements.

Normally, a function ends when it reaches a return statement. But functions can also contain more than one return, which allows us to stop the function and send back a result as soon as we know what that result should be.

Here’s a simple example:

The function stops running the moment it hits a return. To understand why this feature might be useful, consider the below code which computes the length of each string in a list.

This works, but it’s a bit cumbersome as we are keeping track of the result variable throughout. We can simplify this code substantially using multiple return statements:

Now the logic is much cleaner:

  • If the list is empty, return immediately.
  • If we find a bad entry, return immediately.
  • Otherwise, return the processed list.

The key idea here is that a return statement ends the function immediately. Everything after it is skipped. You can think of it as if Python is automatically wrapping everything after a return in an invisible else.

This often makes your functions shorter, easier to read, and can help spot errors and bugs.

Recursion

Another useful feature of the Python programming language is that it allows recursion. A recursive function is a function that calls itself. For example:

In this example, the function countdown calls itself with a smaller number each time. Eventually, it reaches the base case (n == 0) and stops.

Test your Understanding: See if you can modify the above so that, instead of counting down to zero, it counts up to 10 and then prints Blast off!.

Recursion can be useful for problems that can be broken down into smaller versions of the same problem, such as:

  • searching through folders and files,
  • working with tree-like data structures,
  • or classic mathematical problems like factorials.

Exercises

Question 1: Consider the below function.

Without running any code, predict what will happen when you run the following function calls:

  • printing_function(1, 2)
  • printing_function(1, 2, 3, 4)
  • printing_function(1, 2, x=10, y=20)
  • printing_function(1, 2, 3, 4, x=10, y=20)

Do you think the function will error for any of the above inputs? If so, which? Verify your answers by running the code.

Hint: Consider what would happen if you had instead passed my_tuple and my_dict into the function, like we did in the section on arbitrary positional and keyword arguments.

Question 2: Write a function, sumstrings which takes in an arbitrary number of arguments, each a string which represents an integer between 1 and 10 (e.g. one, two, three,… ten), and returns the sum of the strings in numeric form. e.g. sumstrings('ten', 'five', 'eight') should return the integer 23.

Make it so that your function inputs are not case sensitive. I.e. an input of ‘ten’ should be treated the same as ‘tEn’, ‘TEn’, ‘TEN’, etc.

Question 3: Below is a function containing an if statement. The function has one input and one output.

What do you think will be printed when you run each of the following commands?

  • function_with_if(-1)
  • function_with_if(0)
  • function_with_if(1)
  • function_with_if()

Explain your answers and verify them by running the code.

Question 4: Consider the following function, which takes in a positive integer:

Before running the code, try to work out what this function is supposed to do. You may wish to consider the following questions:

  • What values of i will the loop check?
  • What does the expression my_integer % i mean?
  • Under what condition does the function return False?

Test the function on several inputs, such as 2, 7, 9, 15, 17. Based on your tests, describe in plain English what this function is trying to check.

Question 5: Consider the following recursive function:

Run this function on a few different inputs (e.g. "hello", "Python") and try to work out what the function is doing. Once you understand the logic of the function, try rewriting the function using a loop.