At a certain point in time, while working with Python, you might have seen code that uses *args
and **kwargs
as parameters in functions. This feature brings excellent flexibility when developing Python code. I’m going to go over some of the basics of packing arguments into a function, showing how to pass a variable amount of positional arguments and key-value pairs into a function. Feel free to follow along if you have access to a Python interpreter.
Note: Print functions are used for demonstration. These functions don’t create a connection to a device. The goal is to focus on
*args
and**kwargs
.
The current function, device_connection
, has four different required positional parameters and are printed out inside the function.
def device_connection(ip, device_type, username, password):
print("ip: ", ip)
print("device_type: ", device_type)
print("username :", username)
print("password: ", password)
This next step is calling the device_connection
function and it’s passing string arguments at the exact number of parameters in the function. As expected, when calling the function, the data is printed.
>>>
>>> device_connection("10.10.10.2", "cisco_ios", "cisco", "cisco123")
ip: 10.10.10.2
device_type: cisco_ios
username : cisco
password: cisco123
>>>
As mentioned before, these are positional parameters and if the order in which they are called is incorrect, then the output is printed inaccurately. In this case, the device connection is not established if the function was built correctly for connectivity. The next example shows what it looks like when calling the function incorrectly.
>>>
>>> device_connection("cisco_ios", "10.10.10.2", "cisco", "cisco123")
ip: cisco_ios
device_type: 10.10.10.2
username : cisco
password: cisco123
>>>
Notice how in the above output, the IP value is now populated with the device type string while the device_type
is populated with 10.10.10.2.
Another caveat could be if another argument is defined, because the connection requires a different SSH port to establish the connection. If added when calling the function, it fails because the current function was only built to take 4 arguments. Take a look at the example below. The new argument with the port number is added to the function.
>>>
>>> device_connection("cisco_ios", "10.10.10.2", "cisco", "cisco123", "8022")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: device_connection() takes 4 positional arguments but 5 were given
>>>
The next section is going to show how to solve some of these issues.
The basics of packing with *
-operator are compressing multiple values into an agument/parameter. The first line inside the function is using the type function inside a print statement to display the data type returned from the *args
variable. The next few lines have print statements accessing each element of the tuple, which are accessed similar to a list by the index number.
Note: The asterisk (*) symbol is needed to pack the data into a tuple, or it fails and views the parameter as a single argument when the function call is initiated.
def device_connection(*args):
print(type(args))
print("tuple: ", args)
print("ip: ", args[0])
print("device_type: ", args[1])
print("username: ", args[2])
print("password: ", args[3])
The example below calls the device_connection
function, except for this time, the parameter used when building the function is *args
.
>>>
>>> device_connection("10.10.10.2", "cisco_ios", "cisco", "cisco123")
<class 'tuple'>
tuple: ('10.10.10.2', 'cisco_ios', 'cisco', 'cisco123')
ip: 10.10.10.2
device_type: cisco_ios
username: cisco
password: cisco123
>>>
Notice how the function output above printed each argument added to function call and it was “compressed” into the args variable.
Another way of accessing data from a tuple is to use a for loop like the example below.
def device_connection(*args):
print(type(args))
print("tuple: ", args)
for arg in args:
print(arg)
Data can also be assigned to variables and used as input when calling the function.
ip = "10.10.10.2"
device_type = "cisco_ios"
username = "cisco"
password = "cisco123"
Check out the output when the function is called.
>>>
>>> device_connection(ip, device_type, username, password)
<class 'tuple'>
tuple: ('10.10.10.2', 'cisco_ios', 'cisco', 'cisco123')
10.10.10.2
cisco_ios
cisco
cisco123
>>>
Notice how each value was accessed using a for loop when called inside the function without having to provide the index.
Another great thing about using *args
when creating a function is that it can take multiple inputs without having to specify it when building the function. So if another value needs to be processed, then it can be passed as another argument when calling the function.
Note: The
*
asterisk is doing all the “magic” when packing all the arguments; args can be any arbitrary variable.
In this step a new variable called port
is created with a value of 22.
port = 22
The example below shows the output when calling the device_connection
when passing all the same previous variables plus the new variable.
>>>
>>> device_connection(ip, device_type, username, password, port)
<class 'tuple'>
tuple: ('10.10.10.2', 'cisco_ios', 'cisco', 'cisco123', 22)
10.10.10.2
cisco_ios
cisco
cisco123
22
>>>
Previously when passing parameters with no *
-operator, it was by just passing a variable argument. If the same number of arguments aren’t passed when calling the function, it returns with an error. When using the single *
-operator multiple arguments could be passed as options and the data is packed into a tuple.
Another way of using function parameters is to use the **
double asterisk operator and any arbitrary variable when packing key:value
pairs as a parameter into a dictionary.
The function below is using **kwargs
with a variable. Inside the function, it prints the data type and the data that is passed when calling the function.
def device_connection(**kwargs):
print(type(kwargs))
print(kwargs)
The example below shows the output when calling the function with two keyword arguments.
>>>
>>> device_connection(device_type="ios", ip="10.10.10.2")
<class 'dict'>
{'device_type': 'ios', 'ip': '10.10.10.2'}
>>>
Like mentioned before, the (**) asterisk operator is doing the work to pack the data into a variable. Users don’t have to use the variables named args
and kwargs
. They could use an arbitrary variable such as **connection_data
instead of **kwargs
, as shown in the example below.
def device_connection(**connection_data):
print(type(connection_data))
print(connection_data)
The example below shows the output when calling the function using **connection_data
.
>>>
>>> device_connection(device_type="ios", ip="10.10.10.2")
<class 'dict'>
{'device_type': 'ios', 'ip': '10.10.10.2'}
>>>
This next example calls the function again, but with more keyword arguments.
>>>
>>> device_connection(device_type="ios", ip="10.10.10.2", username="cisco", password="cisc123")
<class 'dict'>
{'device_type': 'ios', 'ip': '10.10.10.2', 'username': 'cisco', 'password': 'cisc123'}
>>>
Notice how the data type output is a dictionary and the input arguments are key:value
pairs.
Something else to point out is that *args
can also be used with **kwargs
together in a single function. Using both *args
and **kwargs
together gives the function some more options to choose from in terms of how it can consume the data.
The example below is using a variable containing a dictionary of device data.
csr1 = {
'device_type': 'ios',
'ip': '10.10.10.2',
'username': 'cisco',
'password': 'cisco123',
'port': 8022,
'secret': 'secret'}
The function below is built to take in both types of parameters.
def device_connection(*args, **kwargs):
print("*args: ", type(args))
print("**kwargs: ", type(kwargs))
print("args[0]: ", args[0])
print("args: ", args)
print("kwargs: ", kwargs)
When calling the function, notice how the first argument is the variable. The second and third arguments are keyword arguments.
The first output prints out a tuple
for *args
, the second output is dict
for **kwargs
, the third output gets access to the dictionary by accessing index 0 in the tuple and the last two outputs print out the data “packed” in args
and kwargs
.
>>>
>>> device_connection(csr1, device_type="ios", host="csr1")
*args: <class 'tuple'>
**kwargs: <class 'dict'>
args[0]: {'device_type': 'ios', 'ip': '10.10.10.2', 'username': 'cisco', 'password': 'cisco123', 'port': 8022, 'secret': 'secret'}
args: ({'device_type': 'ios', 'ip': '10.10.10.2', 'username': 'cisco', 'password': 'cisco123', 'port': 8022, 'secret': 'secret'},)
kwargs: {'device_type': 'ios', 'host': 'csr1'}
>>>
The examples shown in this blog use networking data to explore Python parameters in a function using *(for tuples) and **(for dictionaries). There are different ways of building Python functions, and they don’t always need to use * and ** operators for every function built. Sometimes functions can be basic and only need specific data. But when a function takes in variable amount or unknown amount of data, then * and ** operators can come in handy to build more flexible functions.
-Hector
Share details about yourself & someone from our team will reach out to you ASAP!