Three Concepts to Become a Better Python Programmer | by Luay Matalka | Towards Data Science

Python is an extremely versatile and fun language to learn. However, due to the vast amount of tools it provides, it can be difficult to navigate through what’s actually useful to know and what’s just nice to know.
In this tutorial, we will focus on three things that, in my opinion, are incredibly useful to know in Python. We will first discuss what unpacking operators are (* and **) and how to use them to both unpack and pack iterable objects. Then we will move on to *args and **kwargs. And lastly, we will review, in detail, what iteration, iterables, and iterators are and why it’s incredibly beneficial to know what they are.
There will also be a short bonus tip at the end. So let’s get started!
Let’s say we have a list:
num_list = [1,2,3,4,5]
And we define a function that takes in 5 arguments and returns their sum:
def num_sum(num1,num2,num3,num4,num5):    return num1 + num2 + num3 + num4 + num5
And we want to find the sum of all the elements in num_list. Well, we can accomplish this by passing in all the elements of num_list to the function num_sum. Since num_list has five elements in it, the num_sum function contains five parameters, one for each element in num_list.
One way to do this would be to pass the elements by using their index as follows:
num_sum(num_list[0], num_list[1], num_list[2], num_list[3], num_list[4])# 15
However, there is a much easier way to do this, and that’s by using the * operator. The * operator is an unpacking operator that will unpack the values from any iterable object, such as lists, tuples, strings, etc...
For example, if we want to unpack num_list and pass in the 5 elements as separate arguments for the num_sum function, we could do so as follows:
num_sum(*num_list)# 15
And that’s it! The asterisk, *, or unpacking operator, unpacks num_list, and passes the values, or elements, of num_list as separate arguments to the num_sum function.
Note: For this to work, the number of elements in num_list must match the number of parameters in the num_sum function. If they don’t match, we would get a TypeError.
We can also use the asterisk, *, or unpacking operator, with built-in functions in python, such as print:
print(*num_list)# 1 2 3 4 5
Let’s say we have another list:
num_list_2 = [6,7,8,9,10]
And we want to print all the elements in both num_list and num_list_2. We can use the unpacking operator, *, to accomplish this as follows:
print(*num_list, *num_list_2)# 1 2 3 4 5 6 7 8 9 10
Both num_list and num_list_2 are unpacked. Then, all the elements are passed in to print as separate arguments.
We can also create a new list that contains all the elements from num_list and num_list_2:
new_list = [*num_list, *num_list_2]# [1,2,3,4,5,6,7,8,9,10]
num_list and num_list_2 are unpacked, resulting in their elements constituting the elements of the newly made list, new_list.
Note: We could have simply added num_list and num_list_2 to create new_list. However, this was just to portray the functionality of the unpacking operator.
Let’s say that we have a string assigned to the variable name:
name = ‘Michael’
And we want to break this name up into 3 parts, with the first letter being assigned to a variable, the last letter being assigned to another variable, and everything in the middle assigned to a third variable. We can do so as follows:
first, *middle, last = name
And that’s it! Since name is a string, and strings are iterable objects, we can unpack them. The values on the right side of the assignment operator will be assigned to the variables on the left depending on their relative position in the iterable object. As such, the first letter of ‘Michael’ is assigned to the variable first, which would be ‘M’ in this case. The last letter, ‘l’, is assigned to the variable last. And the variable middle will contain all the letters between ‘M’ and ‘l’ in the form of a list: [‘i’, ‘c’, ‘h’, ‘a’, ‘e’].
Note: The first and last variables above are called mandatory variables, as they must be assigned concrete values. The middle variable, due to using the * or unpacking operator, can have any number of values, including zero. If there are not enough values to unpack for the mandatory variables, we will get a ValueError.
For example, if we used the following assignment instead:
first, *middle, last = ‘ma'
Then the variable first will be assigned ‘m’, the variable last will be assigned ‘a’, and the variable middle will just be an empty list since there are no other values to assign to it.
What happens when we try to use the * operator with a dictionary?
num_dict = {‘a’: 1, ‘b’: 2, ‘c’: 3}print(*num_dict)# a b c
Notice how it printed the keys of the dictionary and not the values? To unpack a dictionary, we need to use the ** unpacking operator. However, since each value is associated with a specific key, the function that we pass these arguments to must have parameters with the same names as the keys of the dictionary being unpacked.
For example:
def dict_sum(a,b,c):    return a+b+c
This dict_sum function has three parameters: a, b, and c. These three parameters are named the same as the keys of num_dict. Therefore, once we pass in the unpacked dictionary using the ** operator, it’ll assign in the values of the keys according to the corresponding parameter names:
dict_sum(**num_dict)# 6
Thus, the values, or arguments, for the a, b, and c parameters in dict_sum will be 1, 2, and 3, respectively. And the sum of these three values is 6.
Just like with lists, the ** operator can be used to merge two or more dictionaries:
num_dict = {‘a’: 1, ‘b’: 2, ‘c’: 3}num_dict_2 = {‘d’: 4, ‘e’: 5, ‘f’: 6}new_dict = {**num_dict, **num_dict_2}# {‘a’: 1, ‘b’: 2, ‘c’: 3, ‘d’: 4, ‘e’: 5, ‘f’: 6}
towardsdatascience.com
We can also use the * operator to pack multiple values into a single variable.
For example:
*names, = ‘Michael’, ‘John’, ‘Nancy’# names ['Michael', 'John', 'Nancy']
The reason for using a trailing comma after *names is because the left side of the assignment must be a tuple or list. Therefore, the names variable now contains all the names on the right side in the form of a list.
Note: This is what we do when we define functions that can receive a varying number of arguments! That is the concept of *args and **kwargs!
towardsdatascience.com
For example, let’s say we have a function, names_tuple, that takes in names as arguments and returns them back. However, the number of names that we pass in to this function can vary. Well, we can’t just choose a number of parameters that this function would have since the number of positional arguments can change with each calling of the function. We can instead use the * operator to pack the arguments passed in into a tuple as follows:
def names_tuple(*args):    return argsnames_tuple('Michael', 'John', 'Nancy')# ('Michael', 'John', 'Nancy')names_tuple('Jennifer', 'Nancy')# ('Jennifer', 'Nancy')
No matter what number of positional arguments we pass in when we call the names_tuple function, the *args argument will pack the positional arguments into a tuple, similar to the *names assignment above.
To pass in a varying number of keyword or named arguments, we use the ** operator when defining a function. The ** unpacking operator will pack the varying number of named arguments we pass in into a dictionary.
def names_dict(**kwargs):    return kwargsnames_dict(Jane = 'Doe')# {'Jane': 'Doe'}names_dict(Jane = 'Doe', John = 'Smith')# {'Jane': 'Doe', 'John': 'Smith'}
Note: When using the * operator to create a parameter that receives a varying number of positional arguments when defining a function, it is common to use the parameter name args (and kwargs to receive a varying number of keyword or named arguments). However, any names can be chosen for these parameters.
For a more thorough overview of *args and **kwargs:
towardsdatascience.com
Knowing what iterables and iterators are is extremely useful when programming in Python. Let’s quickly review the differences between iteration, iterables, and iterators, how to identify iterables and iterators, and the benefits of being able to do so.
Broadly speaking, an iterable is something that can be looped over. The process of looping over something, or taking each item of it, one after another, is iteration.
When we use a for loop to loop over a list, the process of looping over this list is iteration (or we are iterating over this list), and the list is the iterable. Tuples, dictionaries, strings, files, and generators are also iterables, as they can also be iterated over.
For example, we can use a for loop to loop over or iterate over a list as follows:
num_list = [1,2,3,4,5]for num in num_list:    print(num)
To be an iterable, an object will have an iter() method. Thus, if an object has an iter() method, then it is an iterable, and thus we can use a for loop to loop over it.
Technically, if an object has a getitem() method it can also be an iterable. However, we will focus on the iter() method here as that is the newer protocol for iteration in python.
We can use the dir() function on num_list to prove that it has the iter() method:
Notice how the list object, num_list, has an __iter__() method, which makes it an iterable.
To know if an object is an iterable, we check if it has an __iter__() method
Methods that have double leading and trailing underscores are hence called dunder methods (also known as magic methods or special methods). These methods are intended to be invoked indirectly, as most python operators have an associated magic method.
An __iter__() method will return an iterator object. To loop over an iterable, such as a list, we don’t usually do that manually. For example, a for loop first calls the __iter__() method on our object which returns an iterator object. The for loop then loops over this iterator by calling the __next__() method. All of this is done in the background.
So this brings us to iterators.
A list is an iterable. But it is not an iterator. If we run the __iter__() method on our list, it will return an iterator. An iterator is an object with a state that remembers where it is during iteration. Iterators also know how to get their next value. They get their next value with the __next__() method.
To know if an object is an iterator, we check if it has a __next__() method
We can first call the iter() function on num_list (the iter() function calls the __iter__() method on the object passed in), which returns an iterator, and then we will use the dir() function on the iterator to view its attributes and methods:
Our iterator, num_list_iterator, has a __next__() method. A list object is not an iterator because it does not have a __next__() method. Thus calling the next() function (which would call the __next__() method on an object if it has one) on a list would result in a TypeError, since our list object is not an iterator and thus does not have a __next__() method. However, a list object is an iterable because it has an __iter__() method, which when called, returns an iterator that will have a __next__() method.
Note how num_list_iterator also has an __iter__() method. As a result, iterators are also iterables, or self-iterables, since calling the __iter__() method on an iterator returns self.
Calling the __next__() method on an iterator will return the next value in the iteration. Thus, calling the __next__() method on the iterator we created from num_list, or num_list_iterator, will return the first value in our iterator. Calling the __next__() method on that same iterator will then return the second value in our iterator (since iterators have a state which remembers where it is during iteration).
Once we’ve went through all the values of our iterator, calling the __next__() method again on our iterator will result in a StopIteration error, since our iterator has now been exhausted. Once an iterator is exhausted, we must make a new iterator via the __iter__() method of the iterable.
Note: Iterators can only go forward. Meaning we can only get the next value in an iterator via the __next__() method. To start from scratch, we would have to create a new iterator object by calling the __iter__() method on the iterable we would like to iterate over.
Knowing whether an object is an iterable (or can be iterated over) can be very helpful while coding in python. For example, knowing if an object is an iterable would tell us if an object can be used in a for loop. Or if the documentation for a Python function states that it takes in an iterable object as an argument, now we know what they mean!
In the above example, we used a for loop to loop over num_list, which was a list of integers starting at 1 and ending at 5, or [1,2,3,4,5]. Well, instead of creating num_list as we did above and looping through it, we could check to see if we can use the range() function to create a range object that we can loop over using a for loop, giving us the same sequence of numbers to iterate over in less code:
The range object is an iterable since it has the __iter__() method! Therefore, it can be iterated over using a for loop, since the for loop will first create the iterator object via the __iter__() method, and then iterate over that iterator object by calling the __next__() method until the iterator returns a StopIteration error, which means the iterator has been exhausted (we don’t see the error).
for num in range(1,6):    print(num)# 12345
Let’s start with the following if/else scenario:
num1, num2 = 5, 10min = Noneif num1 > num2:    min = num2else:    min = num1print(min)# 5
We first set num1 equal to 5, and num2 equal to 10 (note how we assigned multiple values to multiple variables on line one). We then have an if/else statement. If num1 is greater than num2, then the min variable is assigned to num2. Otherwise, min is assigned to num1. Obviously we’re not accounting for num1 and num2 being equal.
We can shorten the code significantly by using ternary operators as follows:
x if C else y
num1, num2 = 5, 10min = num2 if num1 > num2 else num1
And that’s it! C is our condition (num1 > num2), which is evaluated first. If it evaluates to True, then x is evaluated and its value will be returned (and assigned to the variable min). Otherwise, y is evaluated and its value is returned (and assigned to the variable min).
towardsdatascience.com
In this tutorial, we gained some extremely valuable tools and concepts for programming in Python. We first learned how to use * and ** operators to unpack iterable objects and saw many examples of them being used. We also saw how to pack iterable objects using those same operators, and how that relates to the concept of *args and **kwargs. We then reviewed the differences between iteration, iterables, and iterators and how that relates to the __iter__() and __next__() methods. We then saw the benefits of knowing those distinctions and how it can translate to shorter, more concise Python code. Lastly, we learned how ternary operators are used and how they can substantially shorten our code while still being quite intuitive and easy to read.