Mastering Python Functions: A Deep Dive into *args and **kwargs

Studying the rizz of functions in Python

Mastering Python Functions: A Deep Dive into *args and **kwargs

Photo by Chris Ried on Unsplash

Prelude: The Art of Making Your Code Neater and More Flexible

Imagine if you could create functions in Python that adapt to different scenarios without rewriting them every time. That's where the magic of *args and **kwargs comes in. It's like having a magical bag for your function that can hold as many ingredients as you want – making your code both neater and more flexible.

To help you understand *args and **kwargs in Python better, let me show you what the world would look like if these concepts did not exist.
Here, we have a simple function simple_sum that adds two numbers.

def simple_sum(a, b):
    return a + b

result = simple_sum(3, 7)
print(result)

Now, imagine if you wanted to create a function that adds three numbers. You might end up rewriting the function like this:

def simple_sum_three(a, b, c):
    return a + b + c

result_three = simple_sum_three(3, 7, 5)
print(result_three)

What if you wanted to add four, five, or more numbers? You'd have to keep creating new functions for each case, which is neither elegant nor flexible.

To solve this very problem, there is this concept of “argument packing”, in which you use the “*” symbol in the function definition statement to turn an argument into a super argument that can act as a bag and will accommodate/pack all the values you throw at it while calling the function, in one variable.

The same symbol “*” can also be used to unpack a data structure, depending on where it is used and we will learn how to do that later.

This post assumes you're familiar with creating functions in Python, have a basic understanding of regular(positional) and keyword arguments, and know what tuples and dictionaries are in Python.

Argument Packing using *args: Rolling It Up in Style

In Python, when you see an asterisk (*) preceding a parameter in a function definition, it's a signal for argument packing. This means you can throw any number of arguments at the function, and they will be neatly packed into a tuple. It's like having a magical bag for your function, allowing it to adapt to different scenarios without the need for constant rewriting.

def magic_sum(*args):
    result = sum(args)
    return result

# Adding three numbers
result1 = magic_sum(3, 7, 5)
print("Result 1:", result1)  # Expected Output: 15 (3 + 7 + 5)

# Adding five numbers
result2 = magic_sum(1, 2, 3, 4, 5)
print("Result 2:", result2)  # Expected Output: 15 (1 + 2 + 3 + 4 + 5)

# Adding even more numbers
result3 = magic_sum(10, 20, 30, 40, 50, 60)
print("Result 3:", result3)  # Expected Output: 210 (10 + 20 + 30 + 40 + 50 + 60)

In this example, the magic_sum function uses *args for argument packing. You can call this function with different numbers of arguments, and it will neatly pack them into a tuple for addition. The variable args in this case becomes a tuple. It's like having a magic calculator that can handle any number of inputs without changing its formula.

Argument Unpacking: Unleashing the Goodies

On the flip side, when calling a function, you can use the asterisk (*) to indicate that you're unpacking a sequence of values(tuple, list, set, string) and passing its elements as separate values. This is used when you have your data in a collection( for ex: in a tuple) but want to extract each value out of the collection and pass it as a separate argument to a function.

def display_values(a, b, c):
    print("Value of a:", a)
    print("Value of b:", b)
    print("Value of c:", c)

# Unpacking a tuple and passing its elements as separate values
tuple_values = (7, 14, 21)
display_values(*tuple_values) # is same as display_values(7, 14, 21)

You can do this to extract values from multiple collection type data types:

def display_values(a, b, c):
    print("Value of a:", a)
    print("Value of b:", b)
    print("Value of c:", c)

# Unpacking a tuple and passing its elements as separate values
tuple_values = (7, 14, 21)
display_values(*tuple_values)  # Equivalent to display_values(7, 14, 21)

# Unpack a list
list_values = [30, 40, 50]
display_values(*list_values)  # Equivalent to display_values(30, 40, 50)

# Unpack a string (each character becomes a separate argument)
string_values = "XYZ"
display_values(*string_values)  # Equivalent to display_values('X', 'Y', 'Z')

# Unpack a set
set_values = {60, 70, 80}
display_values(*set_values)  # Order might vary, equivalent to display_values(60, 70, 80)

# Unpack a range
range_values = range(3, 6)
display_values(*range_values)  # Equivalent to display_values(3, 4, 5)

You can also change the a,b,c at the function definition part to *values to accept a flexible number of arguments. values will then be a tuple of all the parameters passed. This can be understood as a combination of packing and unpacking at the same time.

def display_values(*values): # packing values into one variable.
    print("Values:", values)

# Unpacking a tuple and passing its elements as separate values
tuple_values = (7, 14, 21)
display_values(*tuple_values)

Keyword Argument Packing using **

Enter the double asterisk (**), a Python operator that brings dictionary packing and unpacking to the party. When a function parameter is prefixed with “**”, it signals that the corresponding arguments should be key-value pairs, neatly packed into a dictionary. It is the same as tuple argument packing but used in cases where the arguments to your function are keyword arguments.

def display_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

# Passing keyword arguments directly in the function call
display_info(name='Alice', age=25, city='Wonderland')

Keyword Argument Unpacking

Now, let's unpack a dictionary and pass its contents as individual keyword arguments to a function. This is used when you have your data in a dictionary but want to extract each key-value pair out of the dictionary and pass it as separate keyword arguments to a function:

def display_person_info(name, age, city):
    print("Name:", name)
    print("Age:", age)
    print("City:", city)

# Unpacking a dictionary with the expected key-value pairs
person_info = {'name': 'Alice', 'age': 25, 'city': 'Wonderland'}
display_person_info(**person_info)

The Symphony: *args and **kwargs in Harmony

Combine the power of *args and **kwargs, treating them as a variable-length positional and keyword argument list, respectively.

def display_information(*args, **kwargs):
    print("Positional Arguments (*args):")
    for arg in args:
        print(arg)

    print("\nKeyword Arguments (**kwargs):")
    for key, value in kwargs.items():
        print(f"{key}: {value}")

# Calling the function with a mix of positional and keyword arguments
display_information(1, 'apple', name='Alice', age=25, city='Wonderland')

In this example, the display_information function takes both: *args to handle any number of positional arguments and **kwargs to handle any number of keyword arguments. The function then prints each type of argument separately.

Grand Finale: Multiple Unpackings in One Act

Another interesting feature in Python is the ability to perform multiple unpackings in a single function call.

def display_values(*args):
    for value in args:
        print(value)

# Multiple unpackings in a single function call
list_values = [1, 2, 3]
tuple_values = (4, 5, 6)
set_values = {7, 8, 9}

display_values(*list_values, *tuple_values, *set_values)

In this example, the display_values function is called with three separate iterable types (a list, a tuple, and a set) using multiple unpackings in a single line. During the function call, all the values from multiple unpackings (*list_values, *tuple_values, *set_values) get collected by the single args argument.

Notes about the naming convention

You don't have to name your arguments "args" or "kwargs," but many Python wizards do. It's like using a special word that everyone in the magic community understands. "args" is for positional arguments, and "kwargs" is for the keyword arguments.