7. Dictionaries#

In this notebook, we cover the following subjects:

  • Defining a Dictionary;

  • Accessing Key-Value Pairs;

  • Modifying a Dictionary;

  • Iterating Through a Dictionary;

  • Nested Dictionaries.


# To enable type hints for lists and dicts, we need to import the following:
from typing import List, Dict

7.1. Defining a Dictionary#

In Python, a dictionary, shortened as dict, is a data type that stores collections of mappings between keys and values. Each key is associated with a single value, forming a key-value pair or item.

A dictionary uses curly brackets {} and defines each entry with a key:value pair, separated by commas. The syntax looks as follows:

my_dict = {
    'key1': 'value1', # item 1
    'key2': 'value2', # item 2
    'key3': 'value3', # item 2
    # Add more key-value pairs as needed
}

Note

A dictionary is a collection that is ordered, mutable, and does not allow duplicate keys.

To create an empty dictionary we can either used {} or the dict() function.

# Using {}
an_empty_dict: Dict = {}
print(an_empty_dict)
{}
# Using dict()
an_other_empty_dict: Dict = dict()
print(an_other_empty_dict)
{}

Here’s an example of a dictionary with some key-value pairs:

my_dict: Dict = {
    'name': 'John',
    'age': 30,
    'city': 'New York',
    'is_student': False
}

print(my_dict)
{'name': 'John', 'age': 30, 'city': 'New York', 'is_student': False}

Note

The type hint for a dictionary is Dict (imported from typing). Like lists, you need to specify the type hints for the data inside the dictionary. The syntax is Dict[key_typehint, value_typehint]

So far, we’ve seen that a dictionary is quite similar to a list. However, while lists associate elements with an integer index, dictionaries use keys that can be of almost any type. Here’s and overview of the properties of dictionaries and lists to understand their differences.

Property

List

Dict Keys

Dict Values

Mutable (can you add add/remove?)

yes

yes

yes

Can contain duplicates

yes

no

yes

Ordered

yes

yes (since Python 3.7)

yes (follows key order)

Can contain

all

immutables

all

7.2. Accessing Key-Value Pairs#

Now, how do we access values inside a dictionary? Well, that’s what keys are for. You can find a specific value by its key, the same way in which you can access list elements by their index. Let’s look at an example, but first, we initialise a dict.

pets: Dict[str, str] = {"dog" : "Max", "cat" : "Lou-Lou", "bird" : "Marco"}

You can access the values in a dictionary using square brackets ([]) and the key, like this:

# What will the output be of this cell?
print(f"I have a dog, his name is {pets['dog']}.")
I have a dog, his name is Max.

There are aslo some methods we can use to access all the keys (.keys()) or all the values (.values()) of a dictionary.

print(pets.keys())
dict_keys(['dog', 'cat', 'bird'])
print(pets.values())
dict_values(['Max', 'Lou-Lou', 'Marco'])

7.2.1. Let’s think!#

Can we still use indices to access elements? Each key-value pair is placed at a certain position, and dictionaries are ordered, so why not? What do you think will happen?

# What happens?

lion_data: Dict[str, int] = {"weight" : 185, "length" : 200, "lifeSpan" : 14}
print(lion_data[0])
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[9], line 4
      1 # What happens?
      3 lion_data: Dict[str, int] = {"weight" : 185, "length" : 200, "lifeSpan" : 14}
----> 4 print(lion_data[0])

KeyError: 0
# Then how do we access the first element?

lion_data: Dict[str, int] = {"weight" : 185, "length" : 200, "lifeSpan" : 14}
print(lion_data["weight"])
# Extra: Can we do this?

print(lion_data[185])

And what about duplicate keys?

# What's going on here?

example_dict: Dict[str, int] = {"a": 1, "a" : 2}
print(example_dict["a"])
print(example_dict)

7.3. Modifying a Dictionary#

7.3.1. Adding#

You already know quite a lot about lists and how you use .append() to add new elements. Do you think you can use .append() to insert a new entry into a dictionary?

# First we initialise a dict
capitals: Dict[str, str] = {"The Netherlands": "Amsterdam", "France": "Paris", "England": "London"}
# Will this work?
capitals.append({"Morocco": "Rabat"})

Note

Remember that the functions dir() and help() can also be used for dictionaries.

What can we do instead? There are two ways:

1. Use the .update() method to insert a new dictionary entry.

capitals.update({"Morocco" : "Rabat"})
print(capitals)

2. Assign a value to a new key directly as if it were already part of the dictionary.

# Remember: dict values can be of any type

capitals["South-Africa"] = ["Pretoria", "Cape Town", "Bloemfontein"]
print(capitals)

7.3.2. Deleting#

Just as we can insert elements into a dictionary, we should also be able to delete elements. How can we do that? There are multiple ways:

  1. Use the del keyword (also works for lists).

  2. Use the .pop() method to delete an element by its key (also works for lists).

  3. Use the .popitem() method to delete the last element.

# First, we initialise a dict of LOTR characters and their roles

lotr_characters: Dict[str, str] = {
    'Frodo': 'Ring Bearer',
    'Sam': 'Gardener',
    'Aragorn': 'Ranger',
    'Legolas': 'Elf Prince',
    'Gimli': 'Dwarf Warrior'
}

1. Use of del

# Delete the character 'Aragorn'
del lotr_characters['Aragorn']

# Dictionary after deletion
print("Dictionary after del:", lotr_characters)

2. Use of .pop()

# Pop the character 'Sam'
popped_role = lotr_characters.pop('Sam')

# Dictionary after pop
print("Dictionary after pop:", lotr_characters)
print("Popped role:", popped_role)

3. Use of .popitem()

# Pop the last inserted element
last_item = lotr_characters.popitem()

# Dictionary after popping the last element
print("Dictionary after popping last item:", lotr_characters)
print("Popped last item:", last_item)

7.3.3. Copying#

Just like with lists, we can use either the .copy() method or the = operator to copy a dictionary. However, you may remember that there’s a difference between these two approaches.

Let’s think! Here is a piece of code that aims to start off with one dictionary but save it in two separate variables to make different changes to each, to then print the two different dictionaries. However, something is not quite right. What’s going on? How can we fix it?

# The Dictionaries
dict1: Dict = {"a" : 1, "b" : 2, "c" : 3, "d" : 4, "e" : 5, "f" : 6}
dict2: Dict = dict1

# Changing dict1
dict1.pop("b")
dict1.pop("d")
dict1.pop("f")

#Changing dict2
dict2.pop("a")
dict2.pop("c")
dict2.pop("e")

# Printing the resulting dictionaries
print(f"dict1 is {dict1}.")
print(f"dict2 is {dict2}.")

7.4. Iterating Through a Dictionary#

Just like we can iterate through lists, we can also iterate through dictionaries. Let’s look at an example.

# Initialise a dict of Florence Pugh movies and their release years
florence_pugh_movies: Dict[str, int] = {
    'Lady Macbeth': 2016,
    'Fighting with My Family': 2019,
    'Midsommar': 2019,
    'Little Women': 2019,
    'Black Widow': 2021,
    'Don’t Worry Darling': 2022,
    'The Wonder': 2022,
    'Oppenheimer': 2023
}

# Let's loop over the dict in a similar way as we would with a list
for item in florence_pugh_movies:
    print(item)

You probably noticed that the output only shows the keys and not the values. How can we print both?

7.4.1. Using the Methods .keys() and .values()#

First, as aforementioned, you can access the keys or values separately using the .keys() and .values() methods, respectively:

# Access keys
for movie in florence_pugh_movies.keys():
    print(f"Key: {movie}")

# Access values
for release_year in florence_pugh_movies.values():
    print(f"Value: {release_year}")

7.4.2. Using the Method .items()#

If you want to access both keys and values simultaneously in a loop, you use the .items() method:

for movie, release_year in florence_pugh_movies.items():
    print(f"{movie} came out in {release_year}.")    

7.5. Nested Dictionaries#

The great thing about dictionaries in Python is that you can create complex data structures using nested dictionaries. This allows you to add layers to your data.

gymnastics_data: Dict = {
    'Simone Biles': {
        'birth_year': 1997,
        'olympic_medals': {
            'gold': 7,
            'silver': 2,
            'bronze': 2
        },
        'events': {
            '2016': ['Rio Olympics', '4 Gold Medals', '1 Bronze Medal'],
            '2020': ['Tokyo Olympics', '1 Silver Medal', '1 Bronze Medal'],
            '2024': ['Paris Olympics', '3 Gold Medals', '1 Silver Medal']
        }
    }
}

We can access the values in nested dicts by using the keys in sequence. The syntax of this looks as follows:

data = {
    'outer': {
        'middle': {
            'inner': 'value'
        }
    }
}

To access 'value', you would use:

inner_value = data['outer']['middle']['inner']

Now, let’s apply this to the dictionary about Simone Biles.

print(f"Simone Biles was born in {gymnastics_data['Simone Biles']['birth_year']}.")
print(f"In the 2016 Rio Olympics, she won {gymnastics_data['Simone Biles']['events']['2016'][1]}.")
print(f"Total gold medals won: {gymnastics_data['Simone Biles']['olympic_medals']['gold']}.")

Note

Since dictionary values can be of any type, it’s possible for a value within a dictionary to be another dictionary. Just remember, keys have to be immutable, so you can’t use a dictionary as a key.

7.6. Exercises#

Let’s practice! Mind that each exercise is designed with multiple levels to help you progressively build your skills. Level 1 is the foundational level, designed to be straightforward so that everyone can successfully complete it. In Level 2, we step it up a notch, expecting you to use more complex concepts or combine them in new ways. Finally, in Level 3, we get closest to exam level questions, but we may use some concepts that are not covered in this notebook. However, in programming, you often encounter situations where you’re unsure how to proceed. Fortunately, you can often solve these problems by starting to work on them and figuring things out as you go. Practicing this skill is extremely helpful, so we highly recommend completing these exercises.

For each of the exercises, make sure to add a docstring and type hints, and do not import any libraries unless specified otherwise.

7.6.1. Exercise 1#

You have a list of dictionaries, each representing a student. Each dictionary contains the student’s full name, year, and a list of seven grades.

Level 1: Use a forloop to calculate the average grade for each student and print the student’s information in the following format. Note that you only need to print the student’s first name, and that the average should be formatted to two digits only:

Format output:

"Student [First Name]'s average grade is [Average Grade]"
students: list = [
    {"name": "LeBron James", "year": 2024, "grades": [68, 65, 62, 71, 47, 69, 33]},
    {"name": "Serena Williams", "year": 2024, "grades": [81, 77, 89, 80, 87, 73, 90]},
    {"name": "Lionel Messi", "year": 2024, "grades": [74, 80, 67, 72, 81, 89, 65]},
    {"name": "Usain Bolt", "year": 2024, "grades": [83, 87, 83, 89, 88, 84, 98]},
    {"name": "Tom Brady", "year": 2024, "grades": [67, 71, 61, 60, 82, 54, 71]},
    {"name": "Simone Biles", "year": 2024, "grades": [95, 92, 97, 91, 99, 92, 98]},
    {"name": "Michael Phelps", "year": 2024, "grades": [35, 48, 52, 41, 66, 40, 59]}
]
# TODO.

Level 2: Expanding on the previous exercise, write a function class_info() that takes a list of student dictionaries as input (same format as Level 1) and returns the following information:

  • The average grade for the entire class;

  • The student with the highest average grade, including their name and grade;

  • The student with the lowest average grade, including their name and grade.

Print this information outside the function in a clear and readable format. Again, note that you only need to print the student’s first name, and that the average should be formatted to two digits only:

Format output:

"Average grade for the entire class: [Class Average Grade]"

"Student with the highest average grade: [Highest Student Name] with an average grade of [Highest Average Grade]"

"Student with the lowest average grade: [Lowest Student Name] with an average grade of [Lowest Average Grade]"
# TODO.

Level 3: Ready for a real challange? We will extend the previous exercise with 3 new functions.

Function 1: Write a function performance_category() that classifies each student into one of three categories based on their average grade:

  • “Excellent”: Average grade ≥ 85

  • “Good”: 70 ≤ Average grade < 85

  • “Needs Improvement”: Average grade < 70

The function should print each student’s name and their performance category in this format:

[First Name] is in the [Performance Category] category

Expected output:

'LeBron is in the Needs Improvement category'
'Serena is in the Good category'
'Lionel is in the Good category'
'Usain is in the Excellent category'
'Tom is in the Needs Improvement category'
'Simone is in the Excellent category'
'Michael is in the Needs Improvement category'

Function 2: Write a function class_std_dev() that calculates and returns the standard deviation of all students’ grades combined.

Standard Deviation Explanation:
It measures how much the grades vary from the average grade. A low standard deviation means grades are close to the average, while a high standard deviation indicates grades are spread out.

σ = standard deviation
xi = each student’s grade
μ = mean of all grades
N = total number of grades

Expected output:


'Standard deviation of class grades: 19.46'

Function3: Write a function class_percentiles() to compute and print the 25th percentile, 50th percentile (median), and 75th percentile of the class grades.

Percentile Explanation:
Percentiles show the grade distribution.

  • The 25th percentile (Q1) means 25% of grades are below that value,

  • the 50th percentile (median) means half of the grades are below,

  • and the 75th percentile (Q3) means 75% of grades are below that value.

Hint:

To calculate the 25 percentile use: 25th percentile:

  • P25 = 25/100*𝑁

N = total number of grades

Expected output:


'25th percentile (Q1): 61'
'50th percentile (Median): 71'
'75th percentile (Q3): 85'
# TODO.

7.6.2. Exercise 2#

A classic, well-known exercise with dictionaries is to implement a word frequency counter.

Level 1: Your task is to create a Python function called word_frequency() that takes a string as input (use sentence as the argument in the function call). The function should count the frequency of each word and return a dictionary where the keys are words and the values are their corresponding frequencies.

Print the dictionary outside the function in a clear and readable format. For this Level, you do not need to worry about uppercase and lowercase variations or punctuation marks.

Input: you pass this argument to the parameter in the function call.

sentence: str = "Wow! The quick, quick brown fox—yes, the very quick fox—jumps over the lazy dog, and the quick fox runs quickly. Quickly, quickly, quickly!"

Output:

{'Wow!': 1, 'The': 1, 'quick,': 1, 'quick': 3, 'brown': 1, 'fox—yes,': 1, 'the': 3, 'very': 1, 'fox—jumps': 1, 'over': 1, 'lazy': 1, 'dog,': 1, 'and': 1, 'fox': 1, 'runs': 1, 'quickly.': 1, 'Quickly,': 1, 'quickly,': 1, 'quickly!': 1}
sentence: str = "Wow! The quick, quick brown fox—yes, the very quick fox—jumps over the lazy dog, and the quick fox runs quickly. Quickly, quickly, quickly!"
# TODO.

Level 2: Copy and adjust the function word_frequency() so that it does not include punctuation and treats words as case-insensitive. For instance, ‘Love’ and ‘love’ should both be counted as ‘love’.

Print the dictionary outside the function in a clear and readable format.

Input: you pass this argument to the parameter in the function call.

sentence: str = "Wow! The quick, quick brown fox—yes, the very quick fox—jumps over the lazy dog, and the quick fox runs quickly. Quickly, quickly, quickly!"

Output:

{'wow': 1, 'the': 4, 'quick': 4, 'brown': 1, 'fox': 3, 'yes': 1, 'very': 1, 'jumps': 1, 'over': 1, 'lazy': 1, 'dog': 1, 'and': 1, 'runs': 1, 'quickly': 4}
# TODO.

Level 3: Let’s turn it up by one notch! Create a function called advanced_analyzer() that processes a given sentence and returns a detailed analysis, including:

A dictionary that maps each unique word to another dictionary containing: (again, we do not include punctuation and treat words as case-insensitive)

  • The count of occurrences of the word.

  • The length of the word.

  • A list of positions (indices) in the sentence where the word appears.

Hint: Use the explanation and helper output snippet between the dashed lines to get inspiration.

Example Input::

sentence: str = "Wow! The quick, quick brown fox—yes, the very quick fox—jumps over the lazy dog, and the quick fox runs quickly. Quickly, quickly, quickly!"

—————————————————————————————

The function should return a dictionary structured like this:

{
    "word": {
        "count": n,
        "length": m,
        "positions": [index1, index2, ...]
    }
}

For example, for the word "quick", the structure might look like:

"quick": {
    "count": 5,
    "length": 5,
    "positions": [3, 4, 11, 13, 17]
}

—————————————————————————————

Expected output:

{
    'wow': {'count': 1, 'length': 3, 'positions': [0]},
    'the': {'count': 4, 'length': 3, 'positions': [1, 6, 10, 15]},
    'quick': {'count': 5, 'length': 5, 'positions': [2, 3, 8, 11, 14]},
    'brown': {'count': 1, 'length': 5, 'positions': [4]},
    'fox': {'count': 4, 'length': 3, 'positions': [5, 9, 12, 18]},
    'yes': {'count': 1, 'length': 3, 'positions': [6]},
    'very': {'count': 1, 'length': 4, 'positions': [7]},
    'jumps': {'count': 1, 'length': 5, 'positions': [8]},
    'over': {'count': 1, 'length': 4, 'positions': [9]},
    'lazy': {'count': 1, 'length': 4, 'positions': [10]},
    'dog': {'count': 1, 'length': 3, 'positions': [11]},
    'and': {'count': 1, 'length': 3, 'positions': [12]},
    'runs': {'count': 1, 'length': 4, 'positions': [13]},
    'quickly': {'count': 4, 'length': 7, 'positions': [14, 17, 18, 19]}
}

7.6.3. Exercise 3#

Level 1: Create a list of book dictionaries called my_books with the following information: title, year published, and number of pages. Then, write a function called find_books_by_year() that takes two arguments: a list of book dictionaries and a year. The function should return a list of book titles published in that year. If no books were published in that year, the function should return “No books found”.

Print the result outside the function in a clear and readable format.

Example input: you pass these arguments to the parameters in the function call.

my_books: List[Dict] = [
    {'title': 'To Kill a Mockingbird', 'year': 1960, 'pages': 281},
    {'title': '1984', 'year': 1949, 'pages': 328},
    {'title': 'Pride and Prejudice', 'year': 1813, 'pages': 432},
    {'title': 'The Great Gatsby', 'year': 1925, 'pages': 180},
    {'title': 'The Catcher in the Rye', 'year': 1951, 'pages': 277}
]

year: int = 1960

Example output:

['To Kill a Mockingbird']
my_books = []

# TODO. 

Level 2: Copy find_books_by_year() and modify in such a way that it takes a list of book dictionaries and a list of years as input. The function should return a dictionary where each year in the input list is a key, and the corresponding value is a list of book titles published in that year. If no books were published in a given year, that year should be included in the dictionary with an empty list as the value.

Print the result outside the function in a clear and readable format.

Example input: you pass these arguments to the parameters in the function call.

my_books: List[Dict] = [
    {'title': 'To Kill a Mockingbird', 'year': 1960, 'pages': 281},
    {'title': '1984', 'year': 1949, 'pages': 328},
    {'title': 'Pride and Prejudice', 'year': 1813, 'pages': 432},
    {'title': 'The Great Gatsby', 'year': 1925, 'pages': 180},
    {'title': 'The Catcher in the Rye', 'year': 1951, 'pages': 277}
]

target_years: List[int] = [1932, 1960, 2021]

Example output:

{
    1932: ['Brave New World'],
    1960: ['To Kill a Mockingbird'],
    2021: []
}
# TODO.

Level 3: Create a function called find_books_advanced() that takes two parameters:

  • books: A list of dictionaries, where each dictionary contains information about a book (title, year, and pages).

  • criteria: A dictionary where the keys are search parameters, which can include any combination of the following:

    • "year": The year the book was published.

    • "min_pages": The minimum number of pages the book should have.

    • "max_pages": The maximum number of pages the book should have.

    • "title_keywords": A list of keywords. The book’s title must contain all of these keywords (case-insensitive).

The function should return a list of book titles that match all the given criteria. If no books match the criteria, return "No books found".

Additional requirements:

  • If the criteria dictionary is empty, the function should return a list of all book titles.

  • The matching for the “title_keywords” should be case-insensitive, and the title must include all the keywords provided.

  • If both "min_pages" and "max_pages" are provided, ensure that the book’s number of pages falls between these two values (inclusive).

Example input: you pass these arguments to the parameters in the function call.

my_books = [
    {'title': 'To Kill a Mockingbird', 'year': 1960, 'pages': 281},
    {'title': '1984', 'year': 1949, 'pages': 328},
    {'title': 'Pride and Prejudice', 'year': 1813, 'pages': 432},
    {'title': 'The Great Gatsby', 'year': 1925, 'pages': 180},
    {'title': 'The Catcher in the Rye', 'year': 1951, 'pages': 277}
]

criteria = {
    "year": 1949,
    "min_pages": 300,
    "max_pages": 350,
    "title_keywords": ["1984"]
}

Example output:

['1984']

If no criteria are provided:

find_books_advanced(my_books, criteria={})

Example output:

['To Kill a Mockingbird', '1984', 'Pride and Prejudice', 'The Great Gatsby', 'The Catcher in the Rye']

# TODO.

Material for the VU Amsterdam course “Introduction to Python Programming” for BSc Artificial Intelligence students. These notebooks are created using the following sources:

  1. Learning Python by Doing: This book, developed by teachers of TU/e Eindhoven and VU Amsterdam, is the main source for the course materials. Code snippets or text explanations from the book may be used in the notebooks, sometimes with slight adjustments.

  2. Think Python

  3. GeekForGeeks