News

When and How to Employ args and kwargs

11 min read
a laptop keyboard with a neon lighting

If you’ve been immersed in Python for a considerable time, chances are you’ve come across code incorporating *args and **kwargs as function arguments. However, even if you haven’t, these are valuable features offering remarkable flexibility in code development. Understanding when and how to employ args and kwargs becomes an essential topic to enhance your programming skills when delving into the step-by-step process of creating a GUI.

This article delves into the utilization of flexible arguments in functions, exploring what they are and how to leverage them effectively.

Args in Python Functions

If you’ve been working with Python, you’ve likely encountered the concept of *args in function definitions. This feature provides a means of handling variable-length argument lists, allowing functions to accept a variable number of arguments.

  • Rather than specifying a fixed number of parameters in a function definition, args enables the acceptance of any number of positional arguments;
  • The asterisk () preceding “args” denotes that multiple arguments can be passed, and they will be captured as a tuple within the function.

Here’s a quick glimpse of how *args works in a function:

def test_function(*args):
    print(type(args))
    for arg in args:
        print(arg)

test_function('a', 'b', 1, 2)

What will be produced as output:

<class 'tuple'>
a
b
1
2

What you observe here is the flexibility of test_function to accept an arbitrary number of arguments. The use of * before a variable transforms multiple inputs into a tuple, allowing access to each argument based on its index. For instance, you can retrieve the first value using args[0], and so on. Keep in mind that in function development, the sequence of inputs is crucial.

Additionally, it’s feasible to combine explicit arguments with *args, as illustrated by:

def test_function(first, second, *args):
    print(first)
    print(second)
    for arg in args:
        print(arg)

The primary distinction lies in the fact that “first” and “second” are obligatory. Both of these alternatives would function effectively:

test_function('first', 2, 'a', 'b', 'c')
test_function('first', 2)

TypeError: test_function() missing 1 required positional argument: ‘second’

Up to this point, it’s evident that *args can be employed to handle a variable number of arguments in a function. Conversely, the reverse scenario is also feasible. Consider a situation where you possess a tuple and wish to utilize its elements as arguments for a function. For instance, envision a function structured like this:

def fixed_args(first, second):
    print(first)
    print(second)

We can utilize the function in the following manner:

vars = ('First', 'Second')
fixed_args(*vars)

This approach proves highly convenient in various scenarios, particularly when dealing with a multitude of inputs where attempting something like the following wouldn’t be practical:

fixed_args(vars[0], vars[1])

It’s crucial to highlight that Python doesn’t mandate the use of *args in its syntax as an argument for a function. You have the freedom to choose any variable name you prefer. However, *args is a convention widely followed by developers. Its adoption enhances code readability and comprehension, both for others and your future self.

The process of converting a tuple (or a list) into distinct function inputs is termed unpacking. However, this capability extends beyond tuples and lists. We can take it a step further and employ a generator, such as range:

def test_function(first, second):
    print(first)
    print(second)

a = range(1, 3)
test_function(*a)

While delving into working with generators is a topic for another tutorial, it’s essential to recognize that the use of * can significantly impact the handling of function arguments.

character sitting at the table and typing on the laptop keyboard, the white digital windows over him

Kwargs in Python

Similar to args, kwargs in Python offer a mechanism for handling function arguments, but with a focus on keyword-arguments instead of tuples or lists. Unlike args, where the order matters, kwargs prioritize the labels assigned to each variable.

Consider the following quick example to illustrate the use of kwargs:

def test_kwargs(**kwargs):
    for key, value in kwargs.items():
        print(key, '=>', value)

In this scenario, the function test_kwargs accommodates a variable number of keyword variables. To employ it, a typical usage would resemble the following:

test_kwargs(first=1, second=2)

Resulting in the output:

first => 1
second => 2

Attempting to execute the function without providing keywords would result in an exception. Similar to how *args transformed inputs into a tuple, **kwargs undergo transformation into a dictionary. The key distinction lies in the use of ** instead of a single *.

Mixing required and variable inputs is also possible, as demonstrated by the following example:

def test_function(first, **kwargs):
    print(first)
    print('Number kwargs: ', len(kwargs))

Executing this function produces the following outputs:

>>> test_function(1)
1
Number kwargs:  0
>>>test_function(1, second=2, third=3)
1
Number kwargs:  2

Just as we used *args to unpack a tuple, the ** operator enables the unpacking of a dictionary. Let’s consider a function with required arguments:

def test_unpack(first, second, third):
    print(first)
    print(second)
    print(third)

For unpacking a dictionary, the following approach is employed:

vars = {'second': 2,
    'first': 1,
    'third': 3}

test_unpack(**vars)

Resulting in the output:

1

2

3

It’s noteworthy that the order of variable definition isn’t crucial; the importance lies in the keywords used to construct the dictionary. Alternatively, using a tuple to define variables is also permissible:

vars = (2, 1, 3)
test_unpack(*vars)

Resulting in the output:

2

1

3

With this comprehensive overview, you now grasp how the * and ** operators function to pack and unpack arguments in functions. The key takeaway is that * is utilized to transform a tuple or list into function arguments with a specific order, while ** transforms a dictionary into keyword arguments where order is insignificant, and the label is crucial.

Functions that accept arguments with either * or ** can accommodate a variable number of arguments. The former caters to arguments in a specific order, while the latter handles keyword arguments. As you consider incorporating args and kwargs into your functions, it’s essential to be mindful of the implications.

The Appropriate Use of args and kwargs in Your Code

When expanding your repertoire in programming, there’s a natural inclination to apply newly acquired knowledge at every opportunity. However, it’s crucial to discern the consequences and benefits of employing args and kwargs in your code. Let’s examine this by considering a function that calculates the area of a triangle. Initially defined as follows:

def area(base, height):
    return base*height/2

The code above clearly conveys its purpose. However, if one decides to incorporate args, the function can be rewritten as:

def area(*args):
    return args[0]*args[1]/2

While both examples are functionally equivalent, the latter introduces a level of complexity that may hinder understanding. Additionally, the function becomes capable of accepting any number of arguments. While Python IDEs like PyCharm or VS Code provide insights into a function’s expected arguments, the use of *args can obscure this information, leaving developers unsure about the necessary inputs.

Choosing the Right Moments for args and kwargs in Your Code

Considerations arise not just in the creation of code but also in its readability, especially when others engage with your work. While the previously developed functions may consist of just two lines, envision the challenge someone might face when encountering a more intricate function. How can they discern the number and specifics of the required arguments?

The quandaries mentioned above are not exclusive to *args; a similar concern emerges with the utilization of **kwargs. Recognizing that good code extends beyond functionality, it’s imperative to prioritize readability and quick comprehension. As you navigate the decision of when to employ args and kwargs, keep in mind that effective code is not only functional but also easily interpretable by others who may engage with your work.

The Role of Args and Kwargs in Decorators

Acknowledging the impossibility of crafting an exhaustive list of scenarios for employing kwargs and args, let’s delve into specific examples, starting with decorators. In essence, a decorator is a function that envelops another to augment its functionality without altering its core behavior. Returning to the area example:

def area(base, height):
    return base*height/2

This function works for any pair of numbers, including negatives. Suppose we wish to ensure that the function only accepts non-negative arguments without modifying its core. In such cases, a decorator like the following can be developed:

from functools import wraps

def check_positive(func):
    @wraps(func)
    def func_wrapper(*args):
        for arg in args:
            if type(arg) is int or type(arg) is float:
                if arg < 0:
                    raise Exception("Function {} takes only positive arguments".format(func.__name__))
            else:
                raise Exception("Arguments of {} must be numbers".format(func.__name__))
        return func(*args)

    return func_wrapper

To use this decorator, one simply annotates the function with @check_positive, as demonstrated with the area_positive function:

@check_positive
def area_positive(base, height):
    return base*height/2

print(area_positive(1, 2))
print(area_positive(-1, 2))

Now, attempting to use a negative value for the base in the second line will trigger an exception. Notably, the use of *args in the decorator ensures its applicability to any function, not just the area function. For instance, calculating the perimeter of a triangle becomes straightforward:

@check_positive
def perimeter(side1, side2, side3):
    return side1+side2+side3

The flexibility granted by *args (or **kwargs) is invaluable in creating versatile decorators. While some decorator examples in the linked article may focus on fixed argument counts, the use of *args allows for adaptability across various function signatures.

Leveraging args and kwargs in Class Inheritance

An extensively advantageous scenario for the application of args and kwargs emerges when working with classes. When seeking to extend the functionality of classes developed by others, a common strategy involves inheriting these classes and selectively overriding the methods that necessitate modification. This pattern proves particularly prevalent in dealings with substantial libraries or frameworks.

Consider the example of developing a Qt application:

class MainWindow(QMainWindow):
    def __init__(self, *args):
        super(MainWindow, self).__init__(*args)

In this snippet, the specific details of your application follow the class instantiation. Notably, by utilizing *args, there’s no need to delve into the original code to discern the arguments being passed; they are seamlessly forwarded to the underlying QMainWindow class during instantiation. Furthermore, if downstream code is already reliant on QMainWindow, replacing it with MainWindow requires no explicit changes.

In the context of frameworks like Django, where method overrides are commonplace, employing kwargs becomes a potent strategy. For instance, when overriding the save method, a future-proof syntax can be implemented as follows:

def save(self, **kwargs):
    # Your custom code goes here
    super().save(**kwargs)

This approach ensures adaptability for future modifications. Even if certain arguments are currently unused, the acceptance of a flexible number of arguments guarantees program stability if there’s a decision to utilize them in the future. Noteworthy is the deliberate use of **kwargs, enforcing the use of keyword arguments. This choice is particularly apt for functions with numerous arguments, each having a default value, where the focus might be on modifying only a specific one.

character standing over the table with opened laptop on it, the digital windows above him

Embracing Versatility with Flexible Arguments

In various programming scenarios, the need for flexibility in the number of arguments arises. An illustrative example of this is evident in Python’s dict. When creating a dictionary, one can employ syntax such as:

a = dict(one=1, two=2, three=3)

However, the arguments of dict are not fixed, allowing for an alternative approach:

b = dict(first=1, second=2, third=3)
  • The capability of dict to accommodate any keyword argument set is a notable asset;
  • The documentation explicitly indicates that dict can be invoked with dict(**kwarg);
  • This level of adaptability is not confined to built-in structures like dictionaries; it extends to more complex frameworks as well.

Consider Django’s Model, where the __init__ method takes both args and kwargs. This design choice amplifies the framework’s ability to offer a high degree of flexibility during class instantiation. A closer examination of the code reveals numerous checks and loops designed to prepare the object based on the available arguments, showcasing the power and versatility that come with handling both positional and keyword arguments.

Conclusion 

Incorporating a variable number of arguments into functions and methods can imbue your code with a heightened level of flexibility. However, this enhanced flexibility often introduces a trade-off with readability. Navigating the nuances of when to deploy *args or *kwargs in your functions is a skill that develops with practice and, notably, exposure to diverse codebases. Exploring examples within the libraries you regularly engage with can unveil insightful applications of variable arguments, prompting you to wonder about the underlying mechanics when functions seamlessly accommodate varying argument counts.

On the flip side, leveraging the * or ** syntax to pass a tuple or dictionary as arguments to a function presents a powerful technique that can significantly streamline your code. Consider scenarios where data importation using pyyaml results in a dictionary that you wish to seamlessly transmit to a function. Unpacking arguments emerges as a valuable tool, particularly when dealing with functions where you lack direct control over their inner workings. This bidirectional understanding of variable arguments enriches your coding repertoire, providing you with the flexibility to design and interact with functions in a manner best suited to your specific needs.