Do You Really Know *args In Python?

Author:Murphy  |  View: 23998  |  Time: 2025-03-22 23:06:30
Image by Miguel Á. Padriñán from Pixabay

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?

Image by Thomas from Pixabay

*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

Image by Rudy and Peter Skitterians from Pixabay

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.

  1. 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.
  2. 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 .join() function. Here is an article that explains it visually.

Do Not Use "+" to Join Strings in Python

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

Image by Markus Weber from Pixabay

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

Tags: Artificial Intelligence Data Science Programming Python Technology

Comment