cogs and wheels used to represent functions in the args and kwargs in Python article

Argh! What are args and kwargs in Python? [Intermediate Python Functions Series #4]

In the first three articles in this Series, you familiarised yourself with the key terms when dealing with functions. You also explored positional and keyword arguments and optional arguments with default values. In this article, you’ll look at different types of optional arguments. Rather unfortunately, these are often referred to by the obscure names args and kwargs in Python.

Overview Of The Intermediate Python Functions Series

Here’s an overview of the seven articles in this series:

  1. Introduction to the series: Do you know all your functions terminology well?
  2. Choosing whether to use positional or keyword arguments when calling a function
  3. Using optional arguments by including default values when defining a function
  4. [This article] Using any number of optional positional and keyword arguments: *args and **kwargs
  5. Using positional-only arguments and keyword-only arguments: the “rogue” forward slash / or asterisk * in function signatures
  6. Type hinting in functions
  7. Best practices when defining and using functions

Using Any Number of Optional Positional Arguments: *args

The topic of *args may seem weird and difficult. However, it’s neither that strange nor that hard. The name is a bit off-putting, but once you understand what’s going on, *args will make sense.

Let’s dive in by looking at this code:

def greet_people(number, *people):
    for person in people:
        print(f"Hello {person}! How are you doing today?\n" * number)

Did you spot the asterisk in front of the parameter name people? You’ll often see the parameter name *args used for this, such as:

def greet_person(number, *args):

However, what makes this “special” is not the name ‘args’ but the asterisk * in front of the parameter name. You can use any parameter name you want. In fact, it’s best practice to use parameter names that describe the data rather than obscure terms. This is why I chose people in this example.

Let’s go back to the function you defined earlier. Consider the following three function calls:

# 1.
greet_people(3, "James", "Stephen", "Kate")

# 2.
greet_people(2, "Bob")

# 3.
greet_people(5)

All three function calls are valid:

  • The first function call has four arguments: 3, "James", "Stephen", and "Kate"
  • The second function call has two arguments: 2 and "Bob"
  • The last function call has one argument: 5

To understand how this is possible, let’s dig a bit deeper into what the parameter people is. Let’s print it out and also print out its type. I’ll comment out the rest of the function’s body for the time being. I’m showing the output from the three function calls as comments in the code snippet below:

def greet_people(number, *people):
    print(people)
    print(type(people))
    # for person in people:
    #     print(f"Hello {person}! How are you doing today?\n" * number)

# 1.
greet_people(3, "James", "Stephen", "Kate")
# OUTPUT:
# ('James', 'Stephen', 'Kate')
# <class 'tuple'>

# 2.
greet_people(2, "Bob")
# OUTPUT:
# ('Bob',)
# <class 'tuple'>

# 3.
greet_people(5)
# OUTPUT:
# ()
# <class 'tuple'>

The local variable people inside the function is a tuple. Its contents are all the arguments you used in the function calls from the second argument onward. The first argument when you call greet_people() is the positional argument assigned to the first parameter: number. All the remaining arguments are collected in the tuple named people.

In the first function call, the first argument is the integer 3. Then there are three more arguments: "James", "Stephen", and "Kate". Therefore, people is a tuple containing these three strings.

In the second function call, the required positional argument is 2. Then, there’s only one additional argument: "Bob". Therefore, the tuple people contains just one item.

In the final function call, there are no additional arguments. The only argument is the required one which is assigned to number. Therefore, people is an empty tuple. It’s empty, but it still exists!

Let’s go back to the function definition you wrote at the start of this section and look at the output from the three function calls:

def greet_people(number, *people):
    for person in people:
        print(f"Hello {person}! How are you doing today?\n" * number)

# 1.
greet_people(3, "James", "Stephen", "Kate")

# 2.
greet_people(2, "Bob")

# 3.
greet_people(5)

The output from this code is:

Hello James! How are you doing today?
Hello James! How are you doing today?
Hello James! How are you doing today?

Hello Stephen! How are you doing today?
Hello Stephen! How are you doing today?
Hello Stephen! How are you doing today?

Hello Kate! How are you doing today?
Hello Kate! How are you doing today?
Hello Kate! How are you doing today?

Hello Bob! How are you doing today?
Hello Bob! How are you doing today?

Since people is a tuple, you can iterate through it using a for loop. This way, you can perform the same action for each of the optional arguments assigned to the tuple people.

The first function call prints three blocks of output (the ones with James, Stephen, and Kate.) The second function call outputs the lines with Bob in them. The final function call doesn’t print anything since the tuple people is empty.

Therefore, when you add an *args to your function definition, you’re allowing any number of optional positional arguments to be added to the function call. Note that I used the term ‘positional’ in the last sentence. These arguments are collected into the args variable using their position in the function call. All the arguments that come after the required positional arguments are optional positional arguments.

Some rules when using *args

Let’s make a small change to the function definition from earlier:

def greet_people(*people, number):
    for person in people:
        print(f"Hello {person}! How are you doing today?\n" * number)

You’ve swapped the position of the parameters number and *people compared to the previous example. Let’s try this function call:

greet_people("James", "Kate", 5)

This raises the following error:

Traceback (most recent call last):
  File "...", line 5, in <module>
    greet_people("James", "Kate", 5)
TypeError: greet_people() missing 1 required keyword-only argument: 'number'

Note that the error is not raised by the function definition but by the function call. There’s a hint as to what the problem is in the error message. Let’s summarise the problem, and then I’ll expand further. All parameters which follow the *args parameter in the function definition must be used with keyword arguments.

What? And Why?

The parameter *people tells the function that it can accept any number of arguments which will be assigned to the tuple people. Since this could be any number, there’s no way for the program to know when you wish to stop adding these optional arguments and move on to arguments that are assigned to the next parameters, in this case, number.

Let’s fix the function call, and then we’ll come back to this explanation:

def greet_people(*people, number):
    for person in people:
        print(f"Hello {person}! How are you doing today?\n" * number)

greet_people("James", "Kate", number=5)

This code now works and gives the following output:

Hello James! How are you doing today?
Hello James! How are you doing today?
Hello James! How are you doing today?
Hello James! How are you doing today?
Hello James! How are you doing today?

Hello Kate! How are you doing today?
Hello Kate! How are you doing today?
Hello Kate! How are you doing today?
Hello Kate! How are you doing today?
Hello Kate! How are you doing today?

Since the last argument is a named (keyword) argument, it’s no longer ambiguous that the value 5 should be assigned to the parameter name number. The program can’t read your mind! Therefore, it needs to be told when the optional positional arguments end since you can have any number of them. Naming all subsequent arguments removes all ambiguity and fixes the problem.

*args summary

Before moving on, let’s summarise what we’ve learned about *args.

  • You can add a parameter with an asterisk * in front of it when defining a function to show that you can use any number of positional arguments in the function call. You can use none, one, or more arguments matched to the *args parameter
  • All the arguments which match the *args parameter are collected in a tuple
  • There’s nothing special about the name args. You can (in fact, you should) use a more descriptive parameter name in your code. Just add the asterisk * before the parameter name

Using Any Number of Optional Keyword Arguments: **kwargs

When you hear about *args in Python, you’ll often hear them mentioned in the same breath as **kwargs. They always seem to come as a pair: “Args and Kwargs in Python!” So let’s see what **kwargs are with the following example:

def greet_people(**people):
    for person, number in people.items():
        print(f"Hello {person}! How are you doing today?\n" * number)

As with ‘args’, there’s nothing special about the name ‘kwargs’. What makes a kwargs a kwargs (!) is the double asterisk ** in front of the parameter name. You can use a more descriptive parameter name when defining a function with **kwargs.

Let’s use the following three function calls as examples in this section:

# 1.
greet_people(James=5, Mark=2, Ishaan=1)

# 2.
greet_people(Stephen=4)

# 3.
greet_people()

Let’s explore the variable people inside the function. As you did earlier, you’ll print out its contents and its type. The rest of the function body is commented, out and the output from the three function calls is shown as comments:

def greet_people(**people):
    print(people)
    print(type(people))
    # for person, number in people.items():
    #     print(f"Hello {person}! How are you doing today?\n" * number)

# 1.
greet_people(James=5, Mark=2, Ishaan=1)
# OUTPUT:
# {'James': 5, 'Mark': 2, 'Ishaan': 1}
# <class 'dict'>

# 2.
greet_people(Stephen=4)
# OUTPUT:
# {'Stephen': 4}
# <class 'dict'>

# 3.
greet_people()
# OUTPUT:
# {}
# <class 'dict'>

The variable people is a dictionary. You used keyword (named) arguments in the function calls, not positional ones. Notice how the keywords you used when naming the arguments are the same as the keys in the dictionary. The argument is the value associated with that key.

For example, in the function call greet_people(James=5, Mark=2, Ishaan=1), the keyword argument James=5 became an item in the dictionary with the string "James" as key and 5 as its value, and so on for the other named arguments. You can include as many keyword arguments as you wish when you call a function with **kwargs.

You may be wondering where the name ‘kwargs’ comes from. Possibly you guessed this already: KeyWord ARGumentS.

Here’s the original function definition and the three function calls again:

def greet_people(**people):
    for person, number in people.items():
        print(f"Hello {person}! How are you doing today?\n" * number)

# 1.
greet_people(James=5, Mark=2, Ishaan=1)

# 2.
greet_people(Stephen=4)

# 3.
greet_people()

This code gives the following output:

Hello James! How are you doing today?
Hello James! How are you doing today?
Hello James! How are you doing today?
Hello James! How are you doing today?
Hello James! How are you doing today?

Hello Mark! How are you doing today?
Hello Mark! How are you doing today?

Hello Ishaan! How are you doing today?

Hello Stephen! How are you doing today?
Hello Stephen! How are you doing today?
Hello Stephen! How are you doing today?
Hello Stephen! How are you doing today?

Since people is a dictionary, you can loop through it using the dictionary method items(). The first function call prints out three sets of greetings for James, Mark, and Ishaan. The number of times each greeting is printed depends on the argument used. The second call displays four greetings for Stephen. The final call doesn’t display anything since the dictionary is empty.

**kwargs summary

In summary:

  • You can add a parameter with a double asterisk ** in front of it when defining a function to show that you can use any number of keyword arguments in the function call
  • All the arguments which match the **kwargs parameter are collected in a dictionary
  • The keyword becomes the key in the dictionary. The argument becomes the value associated with that key
  • There’s nothing special about the name kwargs. As long as you add the double asterisk ** before the parameter name, you can use a more descriptive name

Combining *args and **kwargs

You now know about *args. You also know about **kwargs. Let’s combine both args and kwargs in Python functions.

Let’s look at this code. You have two teams (represented using the dictionaries red_team and blue_team) and the function adds members to one of the teams. Each member starts off with some number of points:

red_team = {}
blue_team = {}

def add_team_members(team, **people):
    for person, points in people.items():
        team[person] = points

add_team_members(red_team, Stephen=10, Kate=8, Sharon=12)

print(f"{red_team = }")
print(f"{blue_team = }")

What do you think the output will be?

The function definition has two parameters. The second one has the double asterisk ** in front of it, which makes it a ‘kwargs’. This means you can pass any number of keyword arguments which will be assigned to a dictionary with the name people.

Now, let’s look at the function call. There is one positional argument, red_team. There are also three keyword arguments. Remember that you can have as many keyword arguments as you want after the first required positional argument.

In the function definition’s body, people is a dictionary. Therefore, you can loop through it using items(). The variables person and points will contain the key and the value of each dictionary item. In the first iteration of the for loop, person will contain the string "Stephen" and points will contain 10. In the second for loop iteration, person will contain "Kate" and points will be 8. And "Sharon" and 12 will be used in the third loop iteration.

Here’s the output of the code above:

red_team = {'Stephen': 10, 'Kate': 8, 'Sharon': 12}
blue_team = {}

Only red_team has changed. blue_team is still the same empty dictionary you initialised at the beginning. That’s because you passed red_team as the first argument in add_team_members().

Some rules when using *args and **kwargs

As you’ve seen in the previous articles in this series and earlier in this one, there are always some rules on how to order the different types of parameters and arguments. Let’s look at a few of these rules here.

Let’s start with this example:

red_team = {}
blue_team = {}

def add_team_members(**people, team):
    for person, points in people.items():
        team[person] = points

You’ll get an error when you run this code even without a function call:

  File "...", line 4
    def add_team_members(**people, team):
                                   ^^^^
SyntaxError: arguments cannot follow var-keyword argument

Note: the error message in Python versions before 3.11 is different. The error says that the variable keyword parameter – that’s the **kwargs – must come after the other parameters.

Let’s make a change to the function before you explore some other options. You can check whether the name of the team member is already in the team and only add it if it’s not already there:

red_team = {"Stephen": 4}
blue_team = {}

def add_team_members(team, **people):
    for person, points in people.items():
        if person not in team.keys():
            team[person] = points
        else:
            print(f"{person} is already in the team")

add_team_members(red_team, Stephen=10, Kate=8, Sharon=12)

print(f"{red_team = }")
print(f"{blue_team = }")

The output from this code is:

Stephen is already in the team
red_team = {'Stephen': 4, 'Kate': 8, 'Sharon': 12}
blue_team = {}

Notice how "Stephen" is already in the dictionary with a value of 4 so the function doesn’t update it. Now, you can add another team and modify the function so that you can add team members to more than one team at a time in a single function call. People can be in more than one team:

red_team = {}
blue_team = {}
green_team = {}

def add_team_members(*teams, **people):
    for person, points in people.items():
        for team in teams:
            if person not in team.keys():
                team[person] = points
            else:
                print(f"{person} is already in the team.")

add_team_members(red_team, blue_team, Stephen=10, Kate=8, Sharon=12)
add_team_members(red_team, green_team, Mary=3, Trevor=15)
add_team_members(blue_team, Ishaan=8)

print(f"{red_team = }")
print(f"{blue_team = }")
print(f"{green_team = }")

You’re using both *args and **kwargs in this function. When you call the function, you can first use any number of positional arguments (without a keyword) followed by any number of keyword arguments:

  • The positional arguments are assigned to the tuple teams
  • The keyword arguments are assigned to the dictionary people

The output from this code is:

red_team = {'Stephen': 10, 'Kate': 8, 'Sharon': 12, 'Mary': 3, 'Trevor': 15}
blue_team = {'Stephen': 10, 'Kate': 8, 'Sharon': 12, 'Ishaan': 8}
green_team = {'Mary': 3, 'Trevor': 15}

You’ll note that Stephen, Kate, and Sharon are in both the red team and the blue team. Mary and Trevor are in the red and green teams. Ishaan is just in the blue team.

Let’s get back to talking about the rules of what you can and cannot do. You can change the function call from the one you used earlier:

red_team = {}
blue_team = {}
green_team = {}

def add_team_members(*teams, **people):
    for person, points in people.items():
        for team in teams:
            if person not in team.keys():
                team[person] = points
            else:
                print(f"{person} is already in the team.")

add_team_members(Stephen=10, Kate=8, Sharon=12, red_team, blue_team)

The output from this code is the following error:

  File "...", line 13
    add_team_members(Stephen=10, Kate=8, Sharon=12, red_team, blue_team)
                                                                       ^
SyntaxError: positional argument follows keyword argument

You cannot place keyword (named) arguments before positional arguments when you call the function. This makes sense since *teams is listed before **people in the function signature. So, can you swap these over when you define a function? Let’s find out:

red_team = {}
blue_team = {}
green_team = {}

def add_team_members(**people, *teams):
    for person, points in people.items():
        for team in teams:
            if person not in team.keys():
                team[person] = points
            else:
                print(f"{person} is already in the team.")

The answer is “No”:

  File "...", line 5
    def add_team_members(**people, *teams):
                                   ^
SyntaxError: arguments cannot follow var-keyword argument

This is an error with the function definition, not the function call (there is no function call in this code!) Therefore, you must include the *args before the **kwargs when you define a function.

Final Words

There are more combinations of “normal” positional arguments, “normal” keyword arguments, *args, and **kwargs you could try. But we’ll draw a line here in this article as the main objective was to give you a good idea of what these types of arguments are and how you can use them.

Now that you know about args and kwargs in Python functions, you can move on to yet another type of argument. In the next article, you’ll read about positional-only arguments and keyword-only arguments.

Next Article: Using positional-only and keyword-only arguments in Python

Further Reading


Get the latest blog updates

No spam promise. You’ll get an email when a new blog post is published


5 thoughts on “Argh! What are args and kwargs in Python? [Intermediate Python Functions Series #4]”

Leave a Reply