Chapter 6 Dictionaries

This chapter covers the second fundamental data structure in Python: dictionaries, which represent a collection of key-value pairs. They are similar to lists, except that each element in the dictionary is also given a distinct “name” to refer to it by (instead of an index number). Dictionaries are Python’s primary version of maps, which is a common and extremely useful way of organizing data in a computer program—indeed, I would argue that maps are the most useful data structure in programming. This chapter will describe how to create, access, and utilize dictionaries to organize and structure data.

6.1 What is a Dictionary?

A dictionary is a lot like a list, in that it is a (one-dimensional) sequence of values that are all stored in a single variable. However, rather than using integers as the indexes for each of the elements, a dictionary allows you to use a wide variety of different data types (including strings and tuples) as the “index”. These “indices” are called keys, and each is used to refer to a specific value in the collection. Thus a dictionary is an sequence of key-value pairs: each element has a “key” that is used to look up (reference) the “value”.

This is a lot like a real-world dictionary or encyclopedia, in which the words (keys) are used to look up the definitions (values). A phone book works the same way (the names are the keys, the phone numbers are the values),

Dictionaries provide a mapping of keys to values: they specify a set of data (the keys), and how that data “transforms” into another set of data (the values).

Dictionaries are written as literals inside curly braces ({}). Key-value pairs are written with a colon (:) between the key and the value, and each element (pair) in the dictionary is separated by a comma (,):

# A dictionary of ages
ages = {'sarah': 42, 'amit': 35, 'zhang': 13}

# A dictionary of English words and their Spanish translation
english_to_spanish = {'one':'uno', 'two':'dos'}

# A dictionary of integers and their word representations
num_words = {1: 'one', 2: 'two', 3: 'three'}

# Like lists, dictionary values can be of different types
# including lists and other dictionaries!
type_examples = {'integer': 12, 'string': 'dog', 'list': [1,2,3]}

# Each dictionary KEY can also be a different type
type_names = {511: 'an int key', 'hello': 'a string key',  (1,2): 'a tuple key!'}

# Dictionaries can be empty (with no elements)
empty = {}

Style Requirement: Dictionary variables are often named as plurals, but can also be named after the mapping they performed (e.g., english_to_spanish).

Be careful not to name a dictionary dict, which is a reserved keyword (it’s a function used to create dictionaries).

Dictionary keys can be of any hashable type (meaning the computer can consistently convert it into a number). In practice, this means that that keys are most commonly strings, numbers, or tuples. Dictionary values, on the other hand, can be of any type that you want!

Dictionary keys must be unique: because they are used to “look up” values, there has to be a single value associated with each key. But dictionary values can be duplicated: just like how two words may have the same definition in a real-world dictionary!

double_key = {'a': 1, 'b': 2, 'b': 3}
print(double_key)  # {'a': 1, 'b': 3}

double_val = {'a': 1, 'b': 1, 'c': 1}
print(double_val)  # {'a': 1, 'b': 1, 'c': 1}

It is important to note that dictionaries are an unordered collection of key-value pairs! Because you reference a value by its key and not by its position (as you do in a list), the exact ordering of those elements doesn’t matter—the interpreter just goes immediately to the value associated with the key. This also means that when you print out a dictionary, the order in which the elements are printed may not match the order in which you specified them in the literal (and in fact, may differ between script executions or across computers!)

dict_a = {'a':1, 'b;':2}
dict_b = {'b':2, 'a:'1}
dict_a == dict_b  # True, order doesn't matter

The above examples mostly use dictionaries as “lookup tables”: they provide a way of “translating” from some set of keys to some set of values. However, dictionaries are also extremely useful for grouping together related data—for example, information about a specific person:

person = {'first_name': "Ada", 'job': "Programmer", 'salary': 78000, 'in_union': True}

Using a dictionary allows you to track the different values with named keys, rather than needing to remember whether the person’s name or title was the first element!

Dictionaries can also be created from lists of keys and values. To do this, you first use the built-in zip() function to create a non-list collection of tuples (each a key-value pair), and then use the built-in dict() function to create a dictionary out of that collection. Alternatively, the built-in enumerate() function will create an collection with the index of each list element as its key.

keys = ['key0', 'key1', 'key2']
values = ['val0', 'val1', 'val2']
dict(zip(keys, values))  # {'key0': 'val0', 'key1': 'val1', 'key2': 'val2'}
dict(enumerate(values))  # {0: 'val0', 1: 'val1', 2: 'val2'}

6.2 Accessing a Dictionary

Just as with lists, you retrieve a value from a dictionary using bracket notation, but you put the key inside the brackets instead of the positional index (since dictionaries are unordered!).

# A dictionary of ages
ages = {'sarah': 42, 'amit': 35, 'zhang': 13}

# Get the value for the 'amit' key
amit_age = ages['amit']
print(amit_age)  # 35

# Get the value for the 'zhang' key
zhang_age = ages['zhang']
print(zhang_age)  # 13

# Accessing a key not in the dictionary will give an error
print(ages['anonymous'])  # KeyError!

# Trying to look up by a VALUE will give an error (since it's not a key)
print(ages[42])  # KeyError!

To reiterate: you put the key inside the brackets in order to access the value. You cannot directly put in a value in order to determine its key (because it may have more than one!)

It is worth noting that “looking up” a value by its key is a very “fast” operation (it doesn’t take the interpreter a lot of time or effort). But looking up the key for a value takes time: you need to check each and every key in the dictionary to see if it has the value you’re interested in! This concept is discussed in more detail in Chapter 11.

As with lists, you can put any expression (including variables) inside the brackets as long as it resolves to a valid key (whether that key is a string, integer, or tuple).

As with lists, you can mutate (change) the dictionary by assigning values to the bracket-notation variable. This changes the key-value pair to have a different value, but the same key:

person = {'name': "Ada", 'job': "Programmer", 'salary': 78000}

# Assign a new value to the 'job' key
person['job'] = 'Sr Programmer'
print(person['job'])  # Sr Programmer

# Assign value to itself
person['salary'] = person['salary'] * 1.15  # a 15% raise!

# Add a new key-value pair by assigning a value to a key that is not yet
# in the dictionary
person['age'] = 37
print(person)  # {'name': 'Ada', 'job': 'Sr Programmer', 'salary': 89700.0, 'age': 37}

Note that adding new elements (key-value pairs) works differently than lists: with a list, you cannot assign a value to an index that is out of bounds: you need to use the append() method instead. With a dictionary, you can assign a value to a non-existent key. This creates the key, assigning it the given value.

6.3 Dictionary Methods

Dictionaries support a few different operations and methods, though not as many as lists. These include:

# A sample dictionary to demonstrate with
sample_dict = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

# The standard `in` operator checks for operands in the keys, not the values!
'b' in sample_dict  # True, dict contains a key `'b'`
2 in sample_dict  # False, dict does not contain a key `2`

# The get() method returns the value for the key, or a "default" value
# if the key is not in the dictionary
default = -511  # a default value
sample_dict.get('c', default)  # 3, key is in dict
sample_dict.get('f', default)  # -511, key not in dict, so return default

# Remove a key-value pair
sample_dict.pop('d')  # removes and returns the `d` key and its value

# Replace values from one dictionary with those from another
other_dict = {'a':10, 'c': 10, 'n':10}
sample_dict.update(other_dict)  # assign values from other to sample
print(sample_dict)  # {'a': 10, 'b': 2, 'c': 10, 'e': 5, 'n': 10}

sample_dict.update(a=100, c=100)  # update also supports named arguments
print(sample_dict)  # {'a': 100, 'b': 2, 'c': 100, 'e': 5, 'n': 10}

# Remove all the elements
sample_dict.clear()

Dictionaries also include three methods that return list-like sequences of the dictionary’s elements:

sample_dict = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

# Get a "list" of the keys
sample_keys = sample_dict.keys()
print(list(sample_keys))  # ['a', 'b', 'c', 'd', 'e']

# Get a "list" of the values
sample_vals = sample_dict.values()
print(list(sample_vals))  # [1, 2, 3, 4, 5]

# Get a "list" of the key-value pairs
sample_items = sample_dict.items()
print(list(sample_items))  # [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)]

The keys(), values(), and items() sequences are not quite lists (they don’t have all of the list operations and methods), but they do support the in operator and iteration with for loops (see below). And as demonstrated above, they can easily be converted into lists if needed. Note that the items() method produces a sequence of tuples—each key-value pair is represented as a tuple whose first element is the key and second is the value!

6.4 Nesting Dictionaries

Although dictionary keys are limited to hashable types (e.g., strings, numbers, tuples), dictionary values can be of any type—and this includes lists and other dictionaries!

Nested dictionaries are conceptually similar to nested lists, and are used in a similar manner:

# a dictionary representing a person (spacing is for readability)
person = {
  'first_name': 'Alice',
  'last_name': 'Smith',
  'age': 40,
  'pets': ['rover', 'fluffy', 'mittens'],  # value is an array
  'favorites': {  # value is another dictionary
    'music': 'jazz',
    'food': 'pizza',
    'numbers': [12, 42]  # value is an array
  }
}

# Can assign lists or dicts to a new key
person['luggage_combo'] = [1,2,3,4,5]

# person['favorite'] is an (anonymous) dictionary, so can get that dict's 'food'
favorite_food = person['favorites']['food'];

# Get to the (anonymous) 'favorites' dictionary in person, and from that get
# the (anonymous) 'numbers' list, and from that get the 0th element
first_fav_number = person['favorite']['numbers'][0];  # 12

# Since person['favorite']['numbers'] is a list, we can add to it
person['favorite']['numbers'].append(7);  # add 7 to end of the list

The ability to nest dictionaries inside of dictionaries is incredibly powerful, and allows you to define arbitrarily complex information structurings (schemas). Indeed, most data in computer programs—as well as public information available on the web—is structured as a set of nested maps like this (though possibly with some level of abstraction).

The other common format used with nested lists and dictionaries is to define a list of dictionaries where each dictionary has the same keys (but different values). For example:

# Arbitrary list of people's names, heights, and weights
people = [
    {'name': 'Ada', 'height': 64, 'weight': 135},
    {'name': 'Bob', 'height': 74, 'weight': 156},
    {'name': 'Chris', 'height': 69, 'weight': 139},
    {'name': 'Diya', 'height': 69, 'weight': 144},
    {'name': 'Emma', 'height': 71, 'weight': 152}
]

This structure can be seen as a list of records (the dictionaries), each of which have a number of different features (the key-value pairs). This list of feature records is in fact a common way of understanding a data table like you would create as an Excel spreadsheet:

name height weight
Ada 64 135
Bob 74 156
Chris 69 139
Diya 69 144
Emma 71 152

Each dictionary (record) acts as a “row” in the table, and each key (feature) acts as a “column”. As long as all of the dictionaries share the same keys, this list of dictionaries is a table!

When working with large amounts of tabular data, like you might read from a .csv file, this is a good structure to use.

In order to analyze this kind of data table, you most often will loop through the elements in the list (the rows in the table), doing some calculations based on each dictionary:

# How many people are taller than 70 inches?
taller_than_70 = 0
for person in people:  # iterate through the list
    # person is a dictionary
    if person['height'] >= 70:  # each dictionary has a 'height' key
        taller_than_70 += 1  # increment the count

print(taller_than_70)  # 2

This is effective, but not particularly efficient (or simple). We will discuss more robust and powerful ways of working with this style of data table more in a later chapter.

6.5 Dictionaries and Loops

Dictionaries are iterable collections (like lists, ranges, strings, files, etc), and so you can loop through them with a for loop. Note that the basic for ... in ... syntax iterates through the dictionary’s keys (not its values)! Thus it is much more common to iterate through one of the keys(), values(), or items() sequences.

sample_dict = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

# Loop through the keys (implicitly)
for key in sample_dict:
    print(key, "maps to", sample_dict[key])  # e.g., "'a' maps to 1"

# Loop through the keys (explicitly)
for key in sample_dict.keys():
    print(key, "maps to", sample_dict[key])  # e.g., "'a' maps to 1"

# Loop through the values. Cannot directly get the key from this
for value in sample_dict.values():
    print("someone maps to", value)  # e.g., "someone maps to 1"

# Loop through the items (each is a tuple)
for item in sample_dict.items():
    print(item[0], "maps to", item[1])  # e.g., "'a' maps to 1"

It is much more common to use multiple assignment to give the items() tuple elements local variable names, allowing you to refer to those elements by name rather than by index:

# Use this format instead!
for key, value in sample_dict.items():  # implicit  `key, value = item`
    print(key, "maps to", value)  # e.g., "'a' maps to 1

# Better yet, name the local variables after their semantic meaning!
for letter, number in sample_dict.items():
    print(letter, "maps to", number)  # e.g., "'a' maps to 1

Remember that dictionaries are unordered. This means that there is no consistency as to which element will be processed in what order: you might get a then b then c, but you might get c then a then b! If the order is important for looping, a common strategy is to iterate through a sorted list of the keys (produced with the built-in sorted() function):

# Sort the keys
sorted_keys = sorted(sample_dict.keys())

# Iterate through the sorted keys
for key in sorted_keys:
    print(key, "maps to", sample_dict[key])

# Or all in one line!
for key in sorted(sample_dict.keys()):
    print(key, "maps to", sample_dict[key])

6.5.1 Dictionary Comprehensions

Python also supports dictionary comprehensions, similar to _list comprehensions. This is a Python shortcut syntax for creating a new dictionary while looping through an iterable data structure (either a list or a dictionary). A dictionary comprehension uses the basic syntax:

new_dict = {new_key: new_value for value in sequence}

This is a shortcut for the following basic loop:

new_dict = {}  # a new dict (to be filled)
for value in sequence:
    new_key = ... # define new key based on value
    new_value = ... # define new value based on value
    new_dict[new_key] = new_value

Notice that a dictionary comprehension looks a lot like a list comprehension; except that you surround the comprehension in {} instead of [], and you specify the new key and value with a colon : between them.

For example, you could create a new dictionary whose keys are words and whose values are booleans indicating if that word is long:

word_list = ["dog", "cat", "information", "husky"]

# create a new dictionary using a regular loop
word_is_long_dict = {} # empty dictionary
for word in word_list:
    # add a new entry to the dictionary
    word_is_long_dict[word] = len(word) > 5

print(word_is_long_dict)
  # {'dog': False, 'cat': False, 'information': True, 'husky': False}

# create a new dictionary using a comprehension
word_is_long_dict = {word: len(word) > 5 for word in word_list}
print(word_is_long_dict)
  # {'dog': False, 'cat': False, 'information': True, 'husky': False}

You can of course also loop through a dictionary in order to create another dictionary:

sample_dict = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

# swap the keys and values. Assumes values are unique.
swapped_dict = { value: key for key, value in sample_dict.items() }

print(swapped_dict) # {1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e'}

6.6 Which Data Structure Do I Use?

The last two chapters have introduced multiple different data structures built into Python: lists, tuples, and dictionaries. They all represent collections of elements, though in somewhat different ways. So when do you each each type?

  • Use lists for any ordered sequence of data (e.g., if you care about what comes first), or if you are collecting elements of the same general “type” (e.g., a lot of numbers, a lot of strings, etc.). If you’re not sure what else to use, a list is a great default data structure.

  • Use tuples when you need to ensure that a list is immutable (cannot be changed), such as if you want it to be a key to a dictionary or a parameter to a function. Tuples are also nice if you just want to store a small set of data (only a few items) that is not going to change. Finally, tuples may potentially provide an “easier” syntax than lists in certain situations, such as when using multiple assignment.

  • Use dictionaries whenever you need to represent a mapping of data and want to link some set of keys to some set of values. If you want to be able to “name” each value in your collection, you can use a dictionary. If you want to work with key-value pairs, you need to use a dictionary (or some other dictionary-like data structure).