Why you should use Catch-All Unpacking and not Slicing in Python
Improve your Python with Catch-All
When it comes to programming, there is the correct way of doing something, and then there is the right way of doing it. There are many cases within the Python language where this is true.
Lists are a fundamental data structure in Python, with a wide variety of use-cases. There are many instances where we require algorithms to split a list into “first” and “rest” pairs. Which is usually done with the use of indexing and slicing. For example:
names = ["Alice", "Bob", "Carol", "Dave", "Elon"]
alice = names[0]
bob = names[1]
everyone_else = names[2:]
The above code snippet is correct however not right for a number of reasons:
we’ve taken up three lines of code in order to accomplish something so simple
we need to know the ordering in advance
error prone as it can lead to off-by-one errors
Why is it error prone?
What could be the case in the future, is that we may want to change the boundaries or the index values, and forget to do so for the others. This of course can lead to errors or unpredictable results.
Catch-All Unpacking to the rescue
To remedy this situation, Python supports catch-all unpacking through the use of a starred expression. This will allow us to achieve the same result as above, without having to make use of indexing or slicing.
names = ["Alice", "Bob", "Carol", "Dave", "Elon"]
alice, bob, *everyone_else = names
Now isn’t that a thing of beauty! We have now:
shortened it to one line
made it easier to read
no longer have to deal with the error prone brittleness of boundary indexes that must be kept in sync between lines.
More on starred expressions
The great thing about starred expressions is that we can place them in any position. This way you can get the benefits of catch-all unpacking anytime you need to extract one slice:
names = ["Alice", "Bob", "Carol", "Dave", "Elon"]
alice, *everyone_else, elon = names
What if there were no elements to unpack? Well, fortunately catch-all unpacking handles that well too. Used correctly, starred expressions will always result in lists. However if there are no more items to unpack from a sequence, it will result in an empty list []
.
books = ["Harry Potter", "Narnia"]
harry_potter, narnia, *other_books = books
print(harry_potter)
print(narnia)
print(other_books)
Output:
Harry Potter
Narnia
[]
The other great thing about catch-all unpacking is that we can use them on iterators. For example:
movies = iter(["Star Wars", "The Godfather", "The Matrix", "Goodfellas"])
star_wars, the_godfather, *other_movies = movies
Although the above snippet is very basic and should in reality just be replaced with a standard list. A better use-case for this would be if you are dealing with CSV files.
Some Gotchas
There are two gotchas when using catch-all unpacking. The first one is to remember that you must have at least one required part, otherwise you will face a SyntaxError
. For example:
*everyone = names
Will result in:
SyntaxError: starred assignment target must be in a list or tuple
The second is that you cannot make use of multiple catch-all expressions in a single-level unpacking structure:
alice, *some_people, *other_people, elon = names
Which will result in:
SyntaxError: multiple starred expressions in assignment
Catch-All Unpacking with a multi-level structure
The second gotcha is true for single-level structures (lists, tuples). However this plays a little differently if we’re using multi-level structures.
For example:
classrooms = {
"classroom 1": ["Alice", "Bob", "Carol"],
"classroom 2": ["David", "Elon"],
}
(
(class_1, (best_student_1, *other_students_1)),
(class_2, (*other_students_2, best_student_2)),
) = classrooms.items()print(
f"The best student in {class_1} is {best_student_1} not the other {len(other_students_1)}"
)
print(
f"The best student in {class_2} is {best_student_2} not the other {len(other_students_2)}"
)
With the output being:
The best student in classroom 1 is Alice not the other 2
The best student in classroom 2 is Elon not the other 1
This may look a bit freakish with the brackets, however it is much more readable and cleaner than if we were to use slicing and indexing.
Final takeaways
This more-or-less sums up why you should prefer Catch-All Unpacking over Slicing and Indexing. The key takeaways here are:
catch-all is visually cleaner compared to slicing and indexing
it a lot less error prone
starred expressions can appear in any position and will always result in a
list
, containing zero or more elements