Dictionaries

Python has 3 primary types of data: sequences, sets, and mappings. A dictionary is a mapping, or, in other words, a container for multiple mappings of key-value pairs. In specific, mappings are collections of objects organized by key values. What does this mean, effectively? Well, dictionaries do not retain any specific order - after all, they are organized by "keys", not by sequential memory locations like in a sequence. In this lecture we will cover:

1. Initializing a Dictionary
    > General key-value mappings
    > Varying keys
    > Varying values
    > Multiple values per key?
2. Accessing and Mutating Dictionaries
    > Notation for access
    > Mutation possibilities
        > Assignment based, deletion, etc
        > Methods
3. Dictionary Functions and Methods
4. Nesting Dictionaries

Initializing a Dictionary

A dictionary is a mapping. To initialize dictionaries, we create these key-value mappings ourselves. The keys in the key object has to be a hashable type, but the value can be any type.

Let's initialize a dictionary that maps strings to numbers. If we were specific, this dictionary could hold the amount of the shares (integer) a person (string) has in a company.


In [3]:
# Initializing a Dictionary
my_dictionary = {"Mike":1, "John":5}

Can we map two integers? Yes, we can. Keys must be hashable types, and integers are hashable.


In [4]:
# Initializing a dictionary with integer-integer pairs
another_dict = {4:1, 8:2, 9:4, 2:6}

Can we vary the key type across mappings in the dictionary? Yes. The example below creates a string, integer, and float key, and maps them all to integers.


In [5]:
# Initializing a Dictionary with varying key types
varying_dict = {"John":4, 9:5, 4.32:8}

Can we vary the value type? Yes. Also, in most of our examples, we will end up mapping strings to numbers or data structures. Below, we've mapped out string keys to integers and lists.


In [7]:
# Initializing a Dictionary that maps strings to either to an integer or list
last_dict = {"Micah":[1, 2, 5], "Rose":4, "John":9, "Gwen":[5, 3]}

Can we map dictionary to other dictionaries? Yes. Note below that "Ruth" and "Barbara" are keys of final_dict, and that "John" and "Leslie" are keys of the dictionary that Ruth is mapped to.


In [9]:
final_dict = {"Ruth":{"John":[1, 2, 5], "Leslie":6 }, "Barbara":4}

Multiple values cannot be assigned to a key in a dictionary. However, as we experimented above, we can map a key to a data structure that holds multiple values by nature.

Accessing and Mutating Dictionaries

Like any other data structure, dictionaries have certain methods of access and mutation.

Accessing Dictionaries

The common notation for access into a dictionary is by specifying the key, which returns the associated value. Consider the notation:

my_dict[key_here]

This notation will return the value(s) that the explicit key is mapped to in the dictionary. Let's try accessing values from dictionaries below.

First, let us create a dictionary.


In [13]:
# Initializing a Dictionary that maps strings to integers
share_holdings_dict = {"john":5, "michael":4, "rutherford":19}

How do we get the number of shares Michael has in the company? Using the above notation, we write the following.


In [18]:
michael_shares = share_holdings_dict["michael"]
print(michael_shares)


4

Sure enough, we get the number of shares Michael has: 4. This notation will allow you to extract a key of any type. Let's try this example again. This time, let's map first names to last names.


In [17]:
# Initializing a Dictionary that maps strings to strings
computer_scientists_dict = {"Peter":"Norvig", "Donald":"Knuth", "Ada":"Lovelace", "Grace":"Hopper"}

How do we get Ada's last name? Use the same notation from before.


In [27]:
last_name = computer_scientists_dict["Ada"]
print(last_name)


Lovelace

Now, let us maps strings to lists. Recall that we can access values using keys. If a string is our key, then our value is a list.


In [21]:
# Initializaing a Dictionary that maps strings to lists
conversion_rate = {"Moe's Pizza":[5, 13, 4], "Jeanie's Pub":[7, 7, 8]}

How do we access the Moe's conversion rate list? The same notation as always...


In [24]:
# Retrieving the list
rates_list = conversion_rate["Moe's Pizza"]
print(rates_list)


[5, 13, 4]

We already know how to retrive values from keys - we just accessed the conversion rate list for Moe's Pizza. So, how do we access the the "things" inside of a value if it is a data structure (in this case, it is a list). In example: let us assume that the elements in the list represent the conversation rate from the 1st year, 2nd year, and 3rd year of opening business. How we do access Moe's Pizza's third year conversion rates. Here is the shorthand way to do it:


In [25]:
third_year_conv = conversion_rate["Moe's Pizza"][2]
print(third_year_conv)


4

The answer, as you can see, is to simply use bracket notation for this list. The expression

conversion_rate["Moe's Pizza"]

gives us [5, 13, 4]. If we call

[5, 13, 4][2]

we would land 4. This is why we can use the bracket notation after getting the list to begin with.

Mutating Dictionaries

You can mutate a dictionary multiple ways. We could consider:

Change value? Delete key? Add key? Change key?

We won't consider deleting a value or adding a value because neither follow the natural state of a dictionary mapping: a key is never value-less.

To change the value of a key we could simple reassign the value, or perform some operation on it. Use the notation following to reassign the value:

my_dict[key] = new_value

This is the same notation as retrieving the value, but now with an equal sign to specify reassignment.


In [31]:
# Initializing a Dictionary
pothole_dict = {"Morgan St.":4, "Tulsen Blvd.":0, "Michigan Ave.":8}

Now, Tulsen Blvd. has a recorded 9 potholes.


In [34]:
# Changing a value of a key
pothole_dict["Tulsen Blvd."] = 9

Let's change Morgan St. and Mich Ave, too.


In [36]:
# Change Michigan and Morgan
pothole_dict["Michigan Ave."] = 30
pothole_dict["Morgan St."] = 2

How about deleting a key? Let's imagine that somehow, Michigan Avenue gets all of its potholes fixed. We can now remove it from the dictionary. That means, we want to remove a key. To remove a key, we need to delete the key using the "del" keyword. The notation is as follows:

del my_dict[key]

This statement removes your key, and thus its values, from the dictionary.

Let's remove Michigan Avenue using the notation above.


In [40]:
# Initializing a Dictionary
pothole_dict = {"Morgan St.":4, "Tulsen Blvd.":0, "Michigan Ave.":8}
# Delete the reference
del pothole_dict["Michigan Ave."]
# Verify
print(pothole_dict)


{'Tulsen Blvd.': 0, 'Morgan St.': 4}

As we can see, Michigan Ave. is no longer in the dictionary.

Adding a key: To add a key is to simply use the same notation as reassigning a key to a new value. If the dictionary detects that the key you put in does not exist, it will take that key and assign it to the value you assigned. The notation remains the same:

my_dict[key] = new_value

As long as key is not already in the dictionary, it will just create it and map it to the value you sent in anyways.


In [42]:
# Initializing a Dictionary
pothole_dict = {"Morgan St.":4, "Tulsen Blvd.":0, "Michigan Ave.":8}
# Add a new street (Key)
pothole_dict["Wallace St."] = 12
# Verify we added Wallace St.
print(pothole_dict)


{'Tulsen Blvd.': 0, 'Wallace St.': 12, 'Michigan Ave.': 8, 'Morgan St.': 4}

We can also change a key. Imagine we accidentally mapped the wrong name to a set of values. How would we do this? While there isn't a single-liner notation for this, we can think methodically. To assign a new key, is to save the values of the old key into a new variable, deleting the old key afterwards (so that we don't lose the values to begin with). Here's the notation:

my_dict[new_key] = my_dict[old_key]
del my_dict[old_key]

We create a new key (a key that's not already in the dictionary), and then assign it to the value that is mapped to the old_key (my_dict[old_key]). Afterwards, we no longer need the mapping of the old_key to its values, so we use the del operator to delete it and its values. The new_key and its values remain unscathed.

Let's change Morgan St. to Hollywood Blvd.


In [43]:
# Initializing a Dictionary
pothole_dict = {"Morgan St.":4, "Tulsen Blvd.":0, "Michigan Ave.":8}
# Store the values with new key
pothole_dict["Hollywood Blvd."] = pothole_dict["Morgan St."]
# Delete the old key, and the values it was mapped to
del pothole_dict["Morgan St."]
# Verify that Hollywood Blvd. replaced Morgan St.
print(pothole_dict)


{'Hollywood Blvd.': 4, 'Tulsen Blvd.': 0, 'Michigan Ave.': 8}

Instead of mutating by reassignment, we can also mutate by using methods or operators on dictionary values. Below, we will make a dictionary and show a variety of ways to put this into action.


In [5]:
# Initialize a Dictionary that maps Strings to numbers
temp_dict = {"Tulsa":79, "Anchorage":10, "Chicago":65}
# Add 20 to the current temperature, and then reassign
temp_dict["Anchorage"] = temp_dict["Anchorage"] + 20
# Print result
print(temp_dict["Anchorage"])


30

Let's use a method now to change a dicitonary's string values using methods.


In [12]:
# Initialize a Dictionary that maps integer keys to string values
palindrome_dict = {3:"wow", 7:"rotator", 4:"noon"}
# Capitalize all of the palidromes, and reassign each as we go
palindrome_dict[3] = palindrome_dict[3].upper()
palindrome_dict[7] = palindrome_dict[7].upper()
palindrome_dict[4] = palindrome_dict[4].upper()

print(palindrome_dict)


{3: 'WOW', 4: 'NOON', 7: 'ROTATOR'}

Dictionary Functions and Methods

There are built-in functions that can be used with dictionaries, and methods that belong inherently to the dictionary library class in Python.

Dictionary Functions

We will discuss the dict() function. The dict function is documented the following way:

dict(**kwarg)
dict(mappings, **kwarg)
dict(iterable, **kwarg)

This can look daunting at first, but it is not too difficult to understand. In essense, the dict() function creates a dictionary. We can supply it with mappings, an iterable, keywords, or simply an argument that evaluates to either a mapping or iterable.

Let's first look at recreating the following dictionary with the 3 types of notations:


In [52]:
# "Normal" Initialization
bball_wins = {"Lakers":20, "Heat":24, "Bulls":26}
print(bball_wins)


{'Lakers': 20, 'Heat': 24, 'Bulls': 26}

First, we showcase the first notation. Here, the equal sign denotes a "name=value" relationship. To make it simple, this notation treats the left variable as the key, and the right as the value.


In [51]:
# Using keywords... dict(**kwargs)
bball_wins2 = dict(Lakers=20, Heat=24, Bulls=26)
print(bball_wins2)


{'Lakers': 20, 'Heat': 24, 'Bulls': 26}

Now the second notation. Here, we've sent in a mapping as the argument (a dictionary itself) into the dict function. It may look odd at first, but it is another way to write the dictionary.


In [50]:
# Using mapping... dict(mapping, **kwargs)
bball_wins3 = dict({"Lakers":20, "Heat":24, "Bulls":26})
print(bball_wins3)


{'Lakers': 20, 'Heat': 24, 'Bulls': 26}

The third notation has a few common variations used. To use it, we need an interable type. However, in the iterable, all objects must also be iterables that contain, at max, two items in themselves. Let's consider the list data structure, which is iterable.


In [49]:
# Using iterable... dict(iterable, **kwargs)
bball_wins4 = dict([["Lakers",20], ["Heat",24], ["Bulls",26]])
print(bball_wins4)


{'Lakers': 20, 'Heat': 24, 'Bulls': 26}

So, what happened? Well, the iterable we sent in happened to have 3 items in itself, which also all happened to be iterables themselves (lists). However, each internal iterable must have 2 objects in it. This is what Python uses to map the iterable:

dict = {}
for k, v in iterable:
    d[k] = v

While this line(s) of code may seem puzzling, we will discuss it again when we reach for-statements. However, it can still be understoof reasonably if you understand the idea of unpacking, which you can learn about right now if you open the "Unpacking.ipynb" iPython notebook (you should right now, before you move on).

You should know it because sometimes dict() will be used with the built-in function zip(). Zip() is fully covered in the built-in functions lecture (which you should also review now before moving on). Consider the code below:


In [54]:
bball_wins5 = dict(zip(["Lakers", "Heat", "Bulls"], (20, 24, 26)))
print(bball_wins5)


{'Lakers': 20, 'Heat': 24, 'Bulls': 26}

What is zip doing? Well, the documentation describes it in this way:

"Make an iterator that aggregates elements from each of the iterables."

It makes an iterator. Well, what does that mean? In short, it takes two iterable objects... these could be two lists, two tuples, a list and a tuple, etc. It takes an element from the first iterable, and the same positioned element in the second iterable, and throws them together in a tuple. Here's a diagram:

zip(["Lakers", "Heat", "Bulls"], (20, 24, 26)]
---> First take first elements of both iterables, and put them together... ("Lakers", 20)
    ---> Now return the tuple... the statement looks like dict([("Lakers", 20)])
        ---> They get mapped together using the dict(mapping, **kwarg) notation
---> Now take seocnd elements of both iterables, and tuple them together... ("Heat", 24)
    ---> refer above
        --> refer above
---> Lastly, put the last elements into a tuple... ("Bulls", 26)
    ---> refer above
        --> refer above

By the end, all of the lists that were created get added as mappings in the dictionary. The result is the dictionary we expected.

Nested Dictionaries

We can nest dictionaries. The access notation gets longer everytime we nest.

Below, we've made a dictionary that maps a first name to a dictionary of possible last names and their shareholdings in a company.


In [57]:
my_dict = {"John":{"Doe":9, "Hansen":13}, "Mariel":{"Stevenson":11, "Somers":2}, "Rocky":9}

How do we get Mariel Somer's shares in the company?

my_dict[key][subkey]....

The same notation access as a list, except now with keys.


In [60]:
# Nested dictionary access
print(my_dict["Mariel"]["Somers"])


2