Understand the Versatility of Asterisks in Python — Know 8 Use Cases
Review of the basic and advanced usages of asterisks
In Python programming, we mostly use letters and numbers but not too many symbols. The most common symbols are operators, including addition (+), subtraction (-), multiplication (*), and division (/). Among these operators, the asterisk symbol has more usages than other operators. In this article, I’d like to review these usages, beyond its use as the multiplication operator.
1. The Power Operator
The first usage is as the power operator. Instead of using the built-in pow function, you can simply use ** to calculate the exponentiation.
>>> pow(5, 3)
125
>>> 5**3
1252. Capture All in Iterable Unpacking
It’s also known as the starred expression. When we work with an iterable, such as a tuple, we can unpack the tuple to retrieve the individual items by creating multiple variables, as shown below.
data = (1, 2, 3, 4)
a, b, c, d = data
# a=1, b=2, c=3, d=4However, it’s also possible to use the starred expression (variable name with an * prefix) to capture multiple items in the unpacking.
a, *b = data
# a=1, b=[2, 3, 4]There are three important things to note in the starred expression.
- You can have only one starred expression in the unpacking. Because it’s intended to capture all items that are not accounted for by other variables, having more than one starred expression will make it impossible to know which items go with which variables.
- The starred expression will create a list object, although we start with a tuple object.
- Starred expressions can be used with more than just tuples, and we can apply this technique for other iterables (e.g., lists, strings).
3. Unpacking Iterables for Creating Variables
In the previous example, the asterisk appears on the left side of the assignment statement to create a new list object. However, it can also appear on the right side of an assignment statement. In this case, it unpacks an existing iterable.
This technique is useful when you create multiple iterables with shared items and some other, different items. Consider the following example. We first create a list object that consists of the shared items and unpack the list object inside other list objects to retrieve the shared items.
shared_items = [1, 2, 3]
items1 = [*shared_items, 4, 5, 6]
items2 = [-3, -2, -1, *shared_items]Although we can use list concatenations to join lists, I personally find this approach more readable. More importantly, it can work with any iterable, while list concatenation only works between lists. Observe the effects below.
>>> shared_numbers = range(3)
>>> [*shared_numbers, 3, 4, 5]
[0, 1, 2, 3, 4, 5]
>>> shared_numbers + [3, 4, 5]
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'range' and 'list'4. Varied Number of Positional Arguments
When you define functions, there are two types of arguments: positional and keyword. Keyword arguments are those that are specified by identifiers, and positional arguments are specified by their positions. In most cases, we define a definite number of positional arguments. However, it’s possible that we can define functions that take a varied number of positional arguments — with the asterisk. See the following for a trivial example before the explanation.
def rounded_sum(*numbers):
total = 0
for number in numbers:
total += round(number)
print(f"Received {numbers} -> rounded sum: {total}")
return total- The
rounded_sumfunction has only one parameter,*numbers, which denotes that when you call this function, you can set any number of positional arguments, as shown below.
>>> rounded_sum(2.4, 3.7, 4.8)
Received (2.4, 3.7, 4.8) -> rounded sum: 11
11- You’ll notice that the varied number of arguments are processed as a tuple object by the function. In the example, we call the function with three numbers, and when we print
numbers, it’s a tuple object consisting of these three numbers.
Although not covered in detail here, it should be noted that the varied number of positional arguments should be after the other positional arguments. Otherwise, Python can’t tell which positional arguments go with which parameters.
5. Unpacking Dictionary
Another common usage of the asterisk is to unpack dictionaries. It’s typically used on the right side of an assignment statement by using an existing dictionary. Suppose that we have the following dictionaries to start with.
science_scores = {"math": 90, "physics": 95, "chemistry": 92}
art_scores = {"english": 93, "spanish": 94}Unlike unpacking lists, tuples, or other iterables, unpacking dictionaries requires the use of two asterisks. As shown below, we create a new dictionary object by unpacking the existing dictionaries.
>>> combined_scores = {**science_scores, **art_scores}
>>> print(combined_scores)
{'math': 90, 'physics': 95, 'chemistry': 92, 'english': 93, 'spanish': 94}The above operation can also be referred to as the merging/updating of dictionaries. There are a few other techniques, which you can find in my previous article, and I’m not expanding this topic here.
6. Varied Number of Keyword Arguments
In addition to a varied number of positional arguments, it’s also possible to define a varied number of keyword arguments in Python functions. The convention is to use **kwargs to denote such a feature. When you see a function signature that uses **kwargs, it means that you can set any number of keyword arguments. Certainly these arguments have to be valid; otherwise, the function can’t use them properly.
There are two common cases where you may want to use **kwargs in a function definition.
First, you don’t know what keyword parameters that the function will take. In this case, the use of **kwargs allows such flexibility. For instance, someone can have the following API function that allows users to send any keyword arguments to the server.
def send_info_to_server(**kwargs):
print(f"Send the info: {kwargs}")
# do something to prepare the informationHere are a few examples of calling this function. As you can see, we can pass any keyword arguments, and those arguments are processed as a dict object inside the function. In other words, when you write your own function involving **kwargs, you process them as dictionary objects.
>>> send_info_to_server(postId="abc", userId="user")
Send the info: {'postId': 'abc', 'userId': 'user'}
>>> send_info_to_server(like=True, status="success")
Send the info: {'like': True, 'status': 'success'}Second, you can make your function definition cleaner by “packing” keyword parameters as **kwargs. For these keyword parameters, they should ideally have known default values that you can specify in the function body. If some parameters don’t have proper default values, it’s better to explicitly list them because making your function signature clear is more important than using **kwargs to make your function signature look clean.
7. Define Keyword-Only Arguments
Another important usage of an asterisk in a function definition is to create keyword-only arguments. As indicated by the name, these arguments can only be specified using keywords. Normally, arguments are parsed by either keyword or position. (There are too many technicalities here in terms of argument parsing, and interested users can refer to the official PEP for a more detailed discussion.).
Let’s consider the following example to illustrate that keyword arguments can be set positionally. As you can see, multiply(5, 4) involves no use of the keyword for the second argument.
def multiply(number, multiplier=1):
return number * multiplier
multiply(5, 4)
multiply(5, multiplier=4)However, to be explicit, we can implement the keyword-only arguments feature with an asterisk, as shown below.
def explicit_multiply(number, *, multiplier=1):
return number * multiplierIn the above function, we use * to indicate that all the arguments behind the asterisk can only be set as keyword arguments. This way, we’re improving the readability of our code by enforcing the use of the defined keywords. With this updated function, calling this function by setting the arguments positionally won’t work. See below.
>>> explicit_multiply(5, multiplier=4)
20
>>> explicit_multiply(5, 4)
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: explicit_multiply() takes 1 positional argument but 2 were given8. Import Everything (Don’t Use it!)
Another common mistake that many people make is to import everything from a module by using the * as the wild card. You may have seen this before somewhere.
from module_abc import *
from module_xyz import *Although it appears that you don’t need to type specific components (e.g., functions and classes), it has two problems. 1) It’s unclear what you’re using from these modules. Readers will be confused. 2) It can have significant side effects, many of which can cause bugs. For instance, both modules may share some functions that happen to have the same name. When everything is imported, the function that you’re using may be different from the one that you want to use. Thus, to avoid these issues, it’s always a best practice to explicitly specify the imported items, as shown below.
from module_abc import fun_a, ClassA
from module_xyz import fun_x, ClassYConclusions
In this article, we reviewed the common eight usages of asterisks in Python. Some are related to others, while some others appear to be on their own. However, they do share the same thing: involving *. When you’re learning a new programming language, it’s generally a good idea to find things that are connected in some ways so that you can reinforce related techniques and know them better.
