Do You Really Know *args In Python?

As one of the most unique syntaxes in Python, *args
will give us lots of flexibility and convenience during Programming. I would say that they reflected what is "Pythonic" and the Zen of Python.
However, I found that they are challenging to be understood by learners. In this article, I'll try my best to explain this iconic concept in Python and provide practical use cases based on my knowledge. I hope it will help you to understand it better.
1. What is "*args" exactly?

*args
stands for "arguments". It allows us to pass any number of positional arguments (will explain later) **** to a function. Inside the function, we can get all of the positional arguments in a tuple. So, we can do whatever with the tuple of arguments in the function.
Here is a simple example of *args
.
Python">def add_up(*numbers):
result = 0
for num in numbers:
result += num
return result
print(add_up(1, 2, 3))
When we call this add_up()
function, we have passed three positional arguments to it. In Python, if we don't specify the names of the arguments, they will be treated as positional arguments. These arguments are determined based on their position, so they are called positional arguments.
In the above example, all the positional arguments 1, 2, 3
were passed into the function and "caught" by the *numbers
parameter. Then, we can access all these arguments from this parameter numbers
. The asterisk *
simply tells Python that this is a *args
type parameter. After that, a simple for-loop adds up all the arguments and prints the result.

The beauty of *arg
, as above-mentioned, is that it can take any number of positional arguments. Therefore, we can pass more arguments if we need to.
print(add_up(1, 2, 3, 4))

Here, we can verify if the variable numbers
is a tuple by adding one line to the original function.
def add_up(*numbers):
print(type(numbers))
result = 0
for num in numbers:
result += num
return result
print(add_up(1, 2, 3))

The reason why Python uses a tuple to contain these arguments is mainly due to its immutability. So, they cannot be modified after creation.
2. Practical Use Cases for *args

Now, let's have a look at the practical use cases for *args
. Since it allows an arbitrary number of arguments, in many scenarios we don't know how many arguments need to pass to a function, which will be its best using scenario.
2.1 Generating Dynamic SQL Queries
One of the common use cases is generating SQL Queries.
Suppose we need to write a function to generate a SELECT statement with unknown numbers of filter conditions. There are two pain points in most of the other programming languages.
- We need to build a collection-type variable such as an array to pack up all the conditions. Then, we need to unpack all the conditions inside the function.
- We don't know the number of conditions. It could be zero. We also need to handle if the condition should start from "WHERE" or "AND". Some developers like to add "WHERE 1=1" to a query so all the conditions can start from AND.
Both of the two pain points can be solved in Python gracefully. Have a look at the code below.
# Generating Dynamic SQL Queries
def create_query(table, *conditions):
sql = f"SELECT * nFROM {table}"
if conditions:
return sql + "nWHERE " + "nAND ".join(conditions)
return sql
The *conditions
is an *args
parameter that takes zero or more conditions. The function builds the SELECT query first, and then checks if there are any arguments in conditions
. If there are, use the .join()
function to build the condition clauses.
Let's see some results. Starting from zero condition.
# Without condition
print(create_query("Users"))

If there is only one condition, the "nAND ".join(conditions)
will give be the only condition that the tuple has. The "AND" will not appear since it is just a connector.
# With one condition
print(
create_query("Users",
"age > 18"
))

If we have multiple conditions, it will work too. The "AND" will be used as a connector between every two condition strings.
# With multiple conditions
print(
create_query("Users",
"age > 18",
"status = 'active'",
"is_vip = 'true'"
))

What an elegant solution!
BTW, if you want to build some really complex SQL query gracefully, there might be better practices than playing with strings. Check out this article to learn more about the tool called sqlglot
.
ChatGPT Doesn't Understand SQL All the Time But This Python Tool Does
Also, if you are not that familiar with the
function. Here is an article that explains it visually.
2.2 Flexible Logging Messages
Suppose we are developing our software that needs to log some messages for many different kinds of messages. The pain point is that these different kinds of messages have different components.
For example, a user login message only needs to tell the activity type and who the user logged in. On the other hand, the file uploaded log message has more components such as the file name, the size and the elapsed time.
Of course, we can archive this without *args
. However, we either need to build a list before we pass all the components to the log_messages()
function, or we have to join the components together as a single string before we pass them to the function.
With *args
the log_messages()
function doesn't really care how many components are there.
from datetime import datetime
def log_messages(*msg):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
full_message = " | ".join(msg)
print(f"[{timestamp}] {full_message}")
# Usage examples
log_messages("User logged in", "Username: Chris")
log_messages("File uploading", "Filename: report.pdf", "/ctao/document/report.pdf")
log_messages("File uploaded", "Filename: report.pdf", "Size: 2MB", "Elapsed Time: 1.3s")
In the above code, we implemented this log_messages()
function. It first gets the current timestamp. Then, all the component strings are joined together with a delimiter to improve the readability. Finally, print the logs.
The usage examples are for demonstration purposes only. In practice, these are more likely to be variables.
The results look great.

2.3 Calculations on Collections
Sometimes, we need to do some calculations on top of some collection types such as lists and sets. In this case, if we want to put several lists into a single list, it definitely works but won't be ideal in terms of readability and flexibility.
In this case, *args
would help.
def find_common_elements(*datasets):
# Initialize the common elements set with the first dataset
common_elements = datasets[0] if datasets else {}
# Intersect with the remaining datasets
for dataset in datasets[1:]:
common_elements.intersection_update(dataset)
return common_elements
# Usage examples:
dataset1 = {1, 2, 3, 4}
dataset2 = {2, 3, 4, 5}
dataset3 = {3, 4, 5, 6}
common_elements = find_common_elements(dataset1, dataset2, dataset3)
print(f"The common elements in the datasets are: {common_elements}")
In the above code, the find_common_elements()
function takes an arbitrary number of sets and will get the intersection of them. It uses the first set to initialise the common set. Then, use the common set to intersect with the rest of the other sets. The result is as follows.

3. Some Python Native Usage of *args

As one of the most unique syntaxes, *args
is unsurprisingly used in a lot of Python built-in modules and their functions. Here are some examples.
Let's have a look at these native examples and why *args
is used in these scenarios. These are very good references that can be used to guide our coding. You won't get any better Python tutorials than Python itself