Destructuring in Python

python

3/17/2023 - 9:00 PM

Destructuring in Python

There are some features in Python that helps us to write simpler code without much of a verbose complexity and most of the time when I have to deal with legacy code, I often find that destructuring is one of those hidden features that is not well spreaded among python developers. Destructuring is kinda old but somehow, we still find lots of examples that don’t take advantage of it so, I’ll try to shed a light into this subject in order for us developers to find new approaches when we unpack data from an iterable object.

When it was introduced?

Unpacking has been available since its initial release in 1991, and has been supported in all versions of Python, which means that we were able to unpack elements from iterable objects, such as tuples or lists, into individual variables since version 1.6. The feature was enhanced through time with PEP 3102 and PEP 3132 that introduced some new rules into destructuring.

PEP 3102

Keyword-Only arguments was introduced by Talin on PEP 3102. At the time arguments in Python were specified either by position or keyword, and this PEP proposed that variable arguments could be added before default arguments in a function, meaning that is desirable that we write functions with both default keyword arguments and positional variables plus varargs in between. For instance, we had this small code:

def compare(a, b, *ignore, key=None):
    if ignore:  # If ignore is not empty
        raise TypeError

On the example above we are defining that the function will only accept variables a, b and an optional argument key. Everything that is in between should be avoided, meaning that if we had ignore defined, then an exception will be raised. Might not be the perfect example in a common sense of usability but explains very well the idea behind this PEP.

PEP 3132

PEP 3132 brought even more flexibility into unpacking, called Extended Iterable Unpacking introduced by Georg Brandl. The idea behind this PEP was to propose a more intuitive syntax into iterable unpacking, including a new catch-all name that should assign a list of items not defined with regular variables. The following explains how this functionality should work:

>>> a, *b, c = range(5)
>>> a
0
>>> c
4
>>> b
[1, 2, 3]

With the above, the variable b stores every element between variables a and c in a list. This is util in algorithms that require to split an iterable object into first and rest part.

Now that we understand a little how and why the destructuring and its enhancements were introduced, we should understand a little more when use it.

When should we use it?

Destructuring can be used every time that we need to unpack elements into individual variables. For instance, we can define a function or method that could return a Tuple type containing different elements to be destructured into smaller elements, even to put data on an specific variable to inform which value we are dealing with a return. Let’s take a look at the example below:

def get_info():
     ellie_info = {"name": 'Ellie Williams', "age": 13}
     joel_info = {"name": 'Joel Miller', "age": 53}
     survivors = True
     return ellie_info, joel_info, survivors
 
*persons, status = get_info()

print(persons)
# [{'name': 'Ellie Williams', 'age': 13}, {'name': 'Joel Miller', 'age': 53}]

print(status)
# True

if status:
    for person in persons:
        print(f"Name: {person['name']}, Age: {person['age']}")

Name: Ellie Williams, Age: 13
Name: Joel Miller, Age: 53 

We defined a function that will return two similar information of persons and an status that informs if they are survivors of a epidemic disaster, the return type will thus be a Tuple. This is interesting because we can return multiple values instead of just one, as we commonly think of and in that case we may return instances of classes instead of a dictionary and the status could be about something that indicate if something went wrong for a reason. Maybe we want to return a large number of objects in the first place, and the * notation shows us that we are dealing with a variable number of elements.

We can also unpack the persons into individual elements, in our case we have just two persons but suppose that we do not know about this and we need just the first two persons from the element persons, in that case we can perform a destructuring of the element, breaking it up into smaller pieces of data getting the first two elements and ignoring the rest, continuing with the example above:

ellie, joel, *_ = persons

print(ellie['name'])
Ellie Williams

print(joel['name'])
Joel Miller

Did you notice that we’ve stored the remaining elements into an underscore varargs variable. That is a good practice to indicate if an element should be ignored, we should use ’_’ to store the remaining elements meant to be avoided from any iterable objects that we are working with.

Destructuring a dict

Yes, it’s possible to destruct a dict into smaller pieces but if we try to destructure the values from a dict directly we’ll get only the keys as strings as we may see in the example below:

print(ellie)
{'name': 'Ellie Williams', 'age': 13}
name, age = ellie

print(name)
# name

print(age)
# age

In that case we should use a small trick to unpack the values into Tuples containing both its key and value by using the dict builtin-function items(), as we may see below:

name, age = ellie.items()
ellie_name, ellie_age = ellie.items()

print(ellie_name)
# ('name', 'Ellie Williams')

print(ellie_age)
# ('age', 13)

Of course we’re not considering the order of the keys in this dict and you should be aware of that, maybe you should loop through each element to find the desired element to unpack.