ruk·si

🐍 Python
Unpacking

Updated at 2022-05-10 23:31

Unpacking helps you to write more readable code e.g. through multiple assignment. Iterator unpacking * is also called "tuple unpacking" but it does apply to all iterables and the ** syntax even to mappings like dictionaries.

Multiple assignment. Does implicit unpacking.

x, y = 10, 20
assert x == 10
assert y == 20

You can also unpack other iterables such as lists.

x, y = [10, 20]
assert x == 10
assert y == 20

# or even strings
x, y = 'hi'
assert x == 'h'
assert y == 'i'

You can also leave the value packed.

point = 10, 20
x, y = point
assert x == 10
assert y == 20

Unpacking iterables is a great alternative to hard coded indices. Multiple assignment is very strict, it you receive more or less than you asked for, it will crash, which is generally a good thing.

# BAD
def reformat_date_bad(date_string: str) -> str:
    """Reformat MM/DD/YYYY string into YYYY-MM-DD string."""
    date = date_string.split('/')
    return f"{date[2]}-{date[0]}-{date[1]}"

assert reformat_date_bad('05/10/2022') == '2022-05-10'

# GOOD
def reformat_date_good(date_string: str) -> str:
    month, day, year = date_string.split('/')
    return f"{year}-{month}-{day}"

assert reformat_date_good('05/10/2022') == '2022-05-10'

Underscore _ signifies that you are not going to use that value.

from typing import Tuple

def get_3d_point() -> Tuple[int, int]:
    return 10, 20, 30

_, _, z = get_3d_point()
assert z == 30

Placing * before an iterable will unpack it in-place.

def show_vectors(x: int, y: int, z: int) -> str:
    return f'<{x}, {y}, {z}>'

assert show_vectors(1, 2, 3) == '<1, 2, 3>'

tuple_vector = (4, 5, 6)
assert show_vectors(*tuple_vector) == '<4, 5, 6>'

list_vector = [7, 8, 9]
assert show_vectors(*list_vector) == '<7, 8, 9>'

# all iterables can be unpacked!
generator_vector = (x * x for x in range(3))
assert show_vectors(*generator_vector) == '<0, 1, 4>'

Placing ** before a mapping will unpack it in-place. This will unpack both the key and the value, which can be used as named arguments in function calls or to merge dictionaries. Using * with dictionaries will unpack just the keys.

def show_vector(x: int, y: int, z: int) -> str:
    return f'<{x}, {y}, {z}>'

dictionary_vector = {'y': 1, 'z': 2, 'x': 3}
assert show_vector(**dictionary_vector) == '<3, 1, 2>'
assert show_vector(*dictionary_vector) == '<y, z, x>'

details = {'first_name': 'Ruksi', 'last_name': 'Korpisara', 'age': 28}
update = {'age': 29, 'genre': 'male'}
updated_details = {**details, **update}
assert len(updated_details.keys()) == 4
assert updated_details['age'] == 29

You can also use * when receiving values. This can make slicing code more readable.

from typing import List

a, b, *rest = range(5)
assert a == 0 and b == 1 and rest == [2, 3, 4]

head, *body, tail = range(5)
assert head == 0 and body == [1, 2, 3] and tail == 4

def numbers() -> List[int]:
    return [1, 2, 3, 4, 5]

first, *between, last = numbers()
assert first == 1
assert between == [2, 3, 4]
assert last == 5

first, *_ = numbers()
assert first == 1

*_, last = numbers()
assert last == 5

Source