In Post #133, we learned how the zip()
function can take multiple lists and combine them into a single iterator of tuples, effectively pairing up the elements. This is like turning columns of data into a series of rows.
But what if we have the opposite problem? What if we start with a list of pairs (our “rows”) and want to separate them back out into their original “columns”?
This process is called “unzipping,” and it uses a clever and elegant trick involving the zip()
function itself, combined with the *
operator. In this post, we’ll demystify this powerful Python idiom.
The Problem: From Rows to Columns
Let’s say we have a list of paired data, where each item is a tuple. This is exactly the kind of data that zip()
produces.
pairs = [(1, 'a'), (2, 'b'), (3, 'c')]
Our goal is to “unzip” this list to get two separate collections: one with all the numbers (1, 2, 3)
and another with all the letters ('a', 'b', 'c')
.
Understanding the *
Operator in Function Calls
Before we get to the solution, we need to understand a different use of the asterisk *
operator. In Post #118, we saw it on the left side of an assignment (a, *b = my_list
) to collect items.
When used inside a function call, it does the opposite. It takes a list or tuple and unpacks it, feeding its elements into the function as separate positional arguments.
For example:
def my_function(arg1, arg2, arg3):
print(f"arg1: {arg1}, arg2: {arg2}, arg3: {arg3}")
my_list = [10, 20, 30]
# This...
my_function(*my_list)
# ...is the exact same as writing this:
my_function(10, 20, 30)
So, if we have our pairs
list, writing zip(*pairs)
is just a shortcut for writing zip(pairs[0], pairs[1], pairs[2])
, which is the same as:zip((1, 'a'), (2, 'b'), (3, 'c'))
The zip(*...)
Idiom
Now, let’s think about what zip()
does when it receives those unpacked tuples as arguments. As we know from our last post, zip()
creates new tuples by taking the first item from each of its arguments, then the second item from each, and so on.
Given the call zip((1, 'a'), (2, 'b'), (3, 'c'))
:
- For its first step, it takes the first item from each argument:
1
from the first tuple,2
from the second, and3
from the third. It yields a new tuple:(1, 2, 3)
. - For its second step, it takes the second item from each argument:
'a'
from the first,'b'
from the second, and'c'
from the third. It yields another tuple:('a', 'b', 'c')
.
The result is an iterator that yields our original “columns” of data!
Putting It All Together
We can combine this zip(*...)
trick with the sequence unpacking we learned in Post #60 to get our final, elegant solution.
pairs = [(1, 'a'), (2, 'b'), (3, 'c')]
# The unzip operation in one line!
numbers, letters = zip(*pairs)
print(f"The numbers tuple is: {numbers}")
print(f"The letters tuple is: {letters}")
The output is exactly what we wanted:
The numbers tuple is: (1, 2, 3)
The letters tuple is: ('a', 'b', 'c')
Here, zip(*pairs)
creates an iterator that produces two tuples. The multiple assignment numbers, letters = ...
then unpacks that iterator, assigning the first tuple to numbers
and the second to letters
.
What’s Next?
The zip(*zipped_list)
idiom is a powerful and very Pythonic way to transpose data—turning rows into columns. While it might seem like magic at first, it’s a logical combination of argument unpacking and the core behavior of the zip()
function.
We’re continuing our tour of Python’s useful built-in functions. What if you have a list of boolean values and you need to know if any of them are True
, or if all of them are True
? In Post #135, we will learn about the concise and efficient any()
and all()
functions.
Author

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins. In my free time, I write blogs on ckdbtech.com