Embercode
✕
Functions and ScopeLesson 16 of 17

45 min read

Variable-length arguments with *args in Python

Why this matters

Many functions need to accept a changing number of values. A function that adds numbers should work with two numbers, with ten, and with none. A function that joins file path parts should accept any number of parts. Writing a separate function for each count is clumsy.

Python has a feature for this. You put a star in front of one parameter, as in *numbers. The name numbers is a choice you make. The star is what matters. Programmers often call this parameter *args, short for "arguments".

If you know Java, this is the same idea as varargs. The difference is the container. Java builds an array for the extra values, and Python builds a tuple.

Words used in this lesson

A parameter is a name in a function header that receives a value. An argument is the actual value you write in the call. In total(1, 2, 3), the three numbers are arguments. The name numbers in the header is the parameter that receives them.

A tuple is an ordered group of values, written with round brackets, such as (1, 2, 3). You can read its items and loop over it. You cannot add, remove or replace items after it is created.

A list is an ordered group of values, written with square brackets, such as [1, 2, 3]. A list can be changed.

The idea: the star packs the values into a tuple

Here is a function that adds any number of values. Run it and read the output. Then the explanation below makes sense.

Python · editable

Output:

total() = 0
total(1) = 1
total(1, 2, 3) = 6
total(*values) = 15
a-b-c

The header def total(*numbers): has a star before numbers. The star tells Python that the caller may pass zero, one or many values. Inside the function, numbers is a tuple. The for loop reads it the same way it would read a list.

Each call builds the tuple for you. The call total(1, 2, 3) creates the tuple (1, 2, 3). The call total() creates an empty tuple, so result stays at 0. You never write the tuple yourself.

The call total(*values) uses the star in a different way. Here the star comes before an argument, and it means "spread this list into separate values". Python takes each item of values and passes it as its own argument. The function receives 4, 5 and 6, and it builds a new tuple from them. In the header the star gathers values into a tuple. In the call the star spreads a list out.

The function join has a normal parameter followed by *parts. The parameter sep always comes first. The starred parameter takes the rest. The call join("-", "a", "b", "c") gives sep the value "-" and gives parts the three strings. The method str.join joins the strings in a tuple or list with a separator, so join passes parts straight to it.

The rules for *args

A function can have at most one starred parameter. The next program shows what happens to a parameter that comes after it.

def show(*items, count):
    return count
 
 
show("a", 1)

Output:

TypeError: show() missing 1 required keyword-only argument: 'count'

Python puts both "a" and 1 into items, because *items takes every value that is left. The parameter count can only be filled by name. A parameter like this is called keyword-only. The call show("a", "b", count=5) works and returns 5.

Python prints a few traceback lines above each error message. These lines, called a traceback, show the file and line where the error happened. This lesson shows only the last line of each error.

A common use of this rule is a function that takes any number of values and also has an option with a default. The option must be named in the call:

Python · editable

Output:

a-b
a b

A function cannot have two starred parameters. The next line is a syntax error. A syntax error means Python cannot read the code at all, so it stops before the function runs.

def f(*a, *b):
    pass

Output:

SyntaxError: * argument may appear only once

Keyword arguments with **options

Two stars in front of a parameter gather extra named values. The parameter **options collects every keyword argument that the header does not name. A keyword argument is a value with a name in the call, such as color="blue". Python stores the collected values in a dictionary. A dictionary holds key and value pairs, and you read a value by its key.

Python · editable

Output:

('Save', {'color': 'blue', 'size': 2})
('Save', {})

Python shows the dictionary with its keys in quotes, inside { and }. When no keyword argument is given, options is an empty dictionary.

Python has no overloading

Overloading means that two functions share one name but have different parameter lists, and the language picks the right one for each call. Python does not do this. If you define a second function with the same name, the second definition replaces the first.

Python · editable

Output:

varargs with 1
varargs with 2

The first single is gone once the second def runs. Python does not warn you. If you want one name for several call shapes, write one function with a starred parameter, or with a default value, and check inside the function what you received.

The caller's data may or may not be shared

A starred parameter is a tuple, and a tuple cannot be changed. The next program tries to change an item.

def only_one(*items):
    items[0] = "changed"
 
 
only_one("x", "y")

Output:

TypeError: 'tuple' object does not support item assignment

A plain list parameter works differently. The function receives the same list object the caller has, so a change inside the function is visible to the caller.

Python · editable

Output:

caller list after rename: ['changed', 'y']

This matters most when a function stores the data. The class below keeps the values it receives. A class is a group of data and the functions that work on it. The function __init__ runs when you create one, and self refers to the object being built.

Python · editable

Output:

tuple team sees: ('Ana', 'Ben')
list team sees: ['Ana', 'Changed']

The call Team(*roster) builds a new tuple from the items of roster. Later changes to roster do not reach the team. The class ListTeam keeps the list it was given, so it sees the change. The common fix is to copy the list inside __init__ with list(names).

The None case

Passing None to a starred parameter gives one value, and that value is None. Passing *None is different. The star asks Python to spread the value into separate arguments, and None has no items to spread.

Python · editable

Output:

1 item(s)
0 item(s)

The next call stops with an error:

describe(*None)

Output:

TypeError: __main__.describe() argument after * must be an iterable, not NoneType

The word iterable means "a value that you can loop over". None is not one. The message says __main__ because that is the name of the module the code runs in.

Type hints for *args

A type hint is a note in the header that says what type each value should have. Python does not check type hints while the code runs. The next example adds hints to total. The hint *numbers: int says that each item should be an int, and -> int says the result is an int.

Python · editable

Output:

3

Python runs the function the same way with or without the hints. A value of the wrong type fails only when an operation on it fails:

print(total(1, "2"))

Output:

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

A third example: an average with no values

An average is a case where zero values need care. The function below adds the values and divides by how many there are.

Python · editable

Output:

average(2, 4, 9) = 5.0
average_or_zero() = 0.0

The function sum adds the items of a tuple or a list. With no values, len(values) is 0. The call average() then divides by zero:

print(average())

Output:

ZeroDivisionError: division by zero

In Java, dividing a double by zero gives Infinity or NaN ("not a number") and the program keeps running. In Python, dividing by zero stops the program with an error, even when the numbers are decimals. The function average_or_zero checks the length first and returns 0.0. Whether 0.0 is the right answer for "no values" is a decision for your program. The check makes the decision visible.

Common mistakes

Leaving out the star. A header without a star takes exactly one value for each parameter. The next example has one parameter and receives three values:

def no_star(numbers):
    return len(numbers)
 
 
no_star(1, 2, 3)

Output:

TypeError: no_star() takes 1 positional argument but 3 were given

Add the star to the header, or spread the values in the call.

Passing a list without the star. The list becomes one item. The function describe from earlier prints 1 item(s) when you pass values without the star, because the whole list is one item. When the function does arithmetic on that item, the error looks like this:

values = [4, 5, 6]
print(total(values))

Output:

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

Use total(*values) to spread the list, or change the header to take one list parameter.

Dividing by the number of values without checking. An empty call gives an empty tuple. Dividing by its length raises ZeroDivisionError. Check len(values) first, as average_or_zero does.

Changing a starred parameter. A tuple cannot be changed, so items[0] = "changed" raises TypeError. If you need to change the values, copy them into a list first with items = list(items).

Passing None with the star. describe(*None) raises TypeError with the word iterable in the message. Check for None at the top of the function when callers might send it, or make sure callers never do.

Keeping a list without a copy. The ListTeam example shows a change from outside reaching the object. Copy the list in __init__ with list(names) when the object must keep its own data.

Practice

Exercise 1. Write max_of(*nums) that returns the largest value. Assume the caller passes at least one value. Test it with max_of(3, 9, 4), max_of(7) and a list arr holding 2, 8, 5, spread with max_of(*arr).

Show the answer

def max_of(*nums):
    best = nums[0]
    for n in nums:
        if n > best:
            best = n
    return best
 
 
print(max_of(3, 9, 4))
print(max_of(7))
arr = [2, 8, 5]
print(max_of(*arr))

The three calls print 9, 7 and 8. The function starts with the first value and replaces it when it finds a bigger one.

Exercise 2. Predict the output of describe() and describe(None), where describe is defined as in the None section and prints len(items).

Show the answer

The first call prints 0 item(s), because the tuple is empty. The second call prints 1 item(s), because None is one value, and the tuple holds that one value.

Exercise 3. Write join_with(sep, *parts) that returns the parts joined with sep. Test it with join_with("-") and with join_with(", ", "x", "y").

Show the answer

def join_with(sep, *parts):
    return sep.join(parts)
 
 
print(repr(join_with("-")))
print(join_with(", ", "x", "y"))

The first call returns an empty string. The function repr shows a value the way Python writes it, so the empty string appears as ''. The second call returns x, y.

Exercise 4. The class below keeps the list it receives. Change the constructor so that a later change to the caller's list does not reach the object.

class Team:
    def __init__(self, names):
        self.members = names

Show the answer

Copy the list in the constructor:

class Team:
    def __init__(self, names):
        self.members = list(names)

With this change, team.members keeps the values it had when the team was created. Changing the caller's list afterwards does not change it.

Exercise 5. A teammate writes def total(*values) and calls it with a list named scores, holding 2, 3, 4. What happens, and what would you change?

Show the answer

The function receives one item, the list itself. The loop then tries to add the list to 0, and Python prints:

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

Change the call to total(*scores), which spreads the list into three values and prints 9. Or change the header to def total(values), which takes the list as one parameter.

Summary

  • A starred parameter, such as *numbers, accepts zero or more values, and Python packs them into a tuple at each call.
  • A function can have at most one starred parameter. Parameters after it can only be filled by name.
  • A star in a call spreads a list or tuple into separate arguments. Without the star, the whole list is one argument.
  • Passing None as one value is different from passing no values, and *None raises a TypeError.
  • A tuple cannot be changed. A plain list parameter is shared with the caller, so copy it when you keep it.
Check yourself: Parameters in Python (7 questions) →Practice this topic: coding problems for Functions and Scope →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Read-only parameters in Python