*args and **kwargs

This lesson will explain what *args and **kwargs are in Python.

1. *args

Let's consider the following function.

def mul(x, y):
  return x * y

This is a simple enough function that takes in two numbers. However, what if you would like a function that can multiply any number of numbers. e.g. 3 * 4 * 5 * 6

One way of doing this is with a list.

def mul(num_list):
  total = 1
  for num in num_list:
    total *= num    # multiply total by num
  return total

list_of_nums = [3,4,5,6]
print(mul(list_of_nums)) # prints 360

This though requires that we create and pass a list of numbers, we can do it another way using *args.

def mul(*args):
  total = 1
  for num in args:
    total *= num
  return total

print(mul(3,4,5)) # prints 60
print(mul(3,4,5,6)) # prints 360

Here we use *args to represent any number of positional arguments. These get packed into what is known as an iterable (in this case it is a tuple) which we can then loop over. The * operator in front of args is known as the unpacking operator.

Note that args is just a name, we could call this something else like numbers.

For example:

def mul(*numbers):
  total = 1
  for num in numbers:
    total *= num
  return total

print(mul(3,4,5)) # prints 60
print(mul(3,4,5,6)) # prints 360

However, it is convention to use args, other programmers will understand better if you stick to this convention!

Here is another example that finds the maximum from a number of numbers.

def maximum(*args):
  # set current max to first item in args
  current_max = args[0]
  # iterate over the rest of args
  for x in args[1:]:
    if x > current_max:
      current_max = x
  return current_max

print(max(1,4,2,7,4)) # prints 7
print(max(1, 4)) # prints 4

2. **kwargs

**kwargs is very similar to *args but instead of being positional arguments, it takes in keyword (named) arguments.

**kwargs will be stored in a dictionary. So you can iterate over the keys(), values() or both using items().

In the example below we iterate over the values. In this case the price of each item.

def bill(**kwargs):
  total = 0
  for price in kwargs.values():
    total += price
  return total

print(bill(bread=5, milk=1.5, orange_juice=1)) # prints 
print(bill(banana=3, pasta=3))
print(bill(bread=5, pasta=3, orange_juice=1, milk=1.5))

In the next example we iterate over both the item and the price using the items() method. Note we have renamed the keyword arguments in the function as bill_items. Again we can call it what we want, we just need to put the ** unpacking operator in front.

def bill(**bill_items):
  total = 0
  for key, value in bill_items.items():
    print(f"Adding {key} at price {value}")
    total += value
  return total

print(bill(bread=5, milk=1.5, orange_juic=1))
print(bill(banana=3, pasta=3))
print(bill(bread=5, pasta=3, orange_juice=1, milk=1.5))

# prints ....
# Adding bread at price 5
# Adding milk at price 1.5
# Adding orange_juic at price 1
# 7.5
# Adding banana at price 3
# Adding pasta at price 3
# 6
# Adding bread at price 5
# Adding pasta at price 3
# Adding orange_juice at price 1
# Adding milk at price 1.5
# 10.5

Again, it is convention to use kwargs, so stick to that.

3. Combining *args and **kwargs

We won't labour on this point, but you can use both in a function, but you need to get the order correct.

Normally we have that positional arguments come before keyword arguments. e.g.

def my_func(x,y,z=10):
  pass

For *args and **kwargs**, *args must come after any positional arguments and **kwargs before any keyword arguments.

However, I would just stick to this order which is common practice:

  1. Positional Arguments
  2. Keyword Arguments
  3. *args arguments
  4. **kwargs arguments

Here are some example function definitions that work and don't work. Try copying them into main.py and running.

3.1 Correct Usage

# This is fine
def my_func(x, y, *args, **kwargs):
    pass
# This is fine
def my_func(x, y, *args):
    pass
# This is fine
def my_func(x, y, **kwargs):
    pass
# This is fine, we now include a keyword argument z
def my_func(x, y, z=10 *args, **kwargs):
    pass

3.2 Incorrect Usage

# This is NOT fine, *args before **kwargs
def my_func(x, y, **kwargs, *args):
    pass
# This is NOT fine, *args before **kwargs
def my_func(x, y, z=10, **kwargs, *args):
    pass
# This is NOT fine, positional arguments come before *args and **kwargs
def my_func(*args, **kwargs, x, y)):
    pass
# This is NOT fine, keyword arguments come before *args and **kwargs
def my_func(x, y, *args, **kwargs, z=10):
  pass

=== TASK ===

Create a function called initials that takes in a persons names and then returns the initials. You should pass the names using *args.

For example for James Marshall Hendrix it should return J.M.H.

Or, for John Ronald Reuel Tolkien it should return J.R.R.T (one *args to rule them all).

Copy this code into a new Python file to get started.

def initials(*args):
  pass

if __name__ == "__main__":
  print(initials("James", "Marshall", "Hendrix")) # should print the return value of "J.M.H"
  print(initials("John", "Ronald", "Reuel", "Tolkien")) # should print the return value of "J.R.R.T"