THEORY AND PRACTICE

  • Input and Output Data
    • Tasks
  • Conditions
    • Tasks
  • For Loop
    • Tasks
  • Strings
    • Tasks
  • While Loop
    • Tasks
  • Lists
    • Tasks
  • Two-Dimensional Arrays
    • Tasks
  • Dictionaries
    • Tasks
  • Sets
    • Tasks
  • Functions and Recursion
    • Tasks

Lesson 8. Dictionaries in python

Introduction to dictionaries

Dictionary in Python: hash table data structure

A dictionary in Python (type dict) is an implementation of a data structure known as a hash table or an associative array. Its key advantage is the incredibly fast access to the elements.

Inside, the dictionary works like this:

  1. You provide a key (for example, the word "apple").
  2. Python calculates a hash from this key — a unique numeric representation.
  3. This number is used as an index to find the location in memory where the value associated with the key is stored (for example, "red fruit").

Thanks to this mechanism, the search, addition and deletion of elements occur almost instantly, regardless of the size of the dictionary.

Basic properties of dictionaries

Collection of key—value pairs

Each element in the dictionary is a pair consisting of a unique key and its associated value. It's like a real dictionary, where the key is a word and the meaning is its definition.

# Example: dictionary with user information
user = {
    "name": "Alex",
    "age": 30,
    "is_admin": True
}
print(user)
                        

The keys are unique

There cannot be two identical keys in the same dictionary. If you try to add a pair with an existing key, the new value will simply overwrite the old one.

config = {"host": "localhost", "port": 8080}
print(f"Original port: {config['port']}")

# Attempt to add a pair with an existing key "port"
config["port"] = 9000
print(f"New port: {config['port']}")
print(f"Entire dictionary: {config}")
                        

Values can be repeated

Unlike keys, the values can be anything and repeated as many times as desired.

# Different students may have the same grades
grades = {
"Ivan": 5,
"Maria": 4,
    "Peter": 5
}
print(grades)
                        

The difference between a dictionary and a list and a set

  • Dictionary vs List (list): In lists, elements are ordered and accessible by numeric index (0, 1, 2...). In dictionaries, elements are accessible by a key, which can be a string, number, or other immutable type. Searching by key in a dictionary (O(1)) is much faster than searching for an item in a list (O(n)).
  • Dictionary vs Set (set): Sets store only unique values (without keys). They are good for checking for the presence of an element and mathematical operations (union, intersection), but not for storing related data.

Practical applications of dictionaries

Dictionaries are used everywhere:

  • JSON data: Web APIs almost always return data in a form that can be easily converted to Python dictionaries.
  • Configuration settings: Storing program parameters.
  • Representation of objects: For example, user, product, order.
  • Caching: Storing the results of "expensive" calculations.
  • Frequency Counting: A quick count of unique items in a collection.

Creating Python dictionaries

The dictionary is empty: creation

Via {}

The most common and preferred method is to use curly braces.

empty_dict_1 = {}
print(f"Type: {type(empty_dict_1)}, Content: {empty_dict_1}")
                        

Via the dict() function

You can also use the built-in dict() function.

empty_dict_2 = dict()
print(f"Type: {type(empty_dict_2)}, Content: {empty_dict_2}")
                        

Dictionary with specified values

Explicit indication of key-value pairs

This is the main way to create a dictionary with initial data.

person = {
    "first_name": "John",
    "last_name": "Doe",
    "age": 25
}
print(person)
                        

Via the dict() function with named arguments

This method is convenient when the keys are simple strings without spaces or special characters.

person_alt = dict(first_name="Jane", last_name="Doe", age=28)
print(person_alt)
                        

Creating a dictionary from lists using zip()

If you have two lists (one for keys, the other for values), they can be "stitched" into a dictionary using zip() and dict().

keys = ["brand", "model", "year"]
values = ["Toyota", "Camry", 2022]
car = dict(zip(keys, values))
print(car)
                        

Dictionary generators (dict comprehension)

It is a powerful and concise way to create dictionaries based on any iterable sequence.

# Create a dictionary where the key is a number and the value is its square
squares ={x: x**2 for x in range(1, 6)}
print(squares) # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
                        

Access to dictionary elements

Getting the key value via []

The main way to access a value is to specify its key in square brackets. Warning: if the key does not exist, it will cause the error KeyError.

user = {"name": "Alex", "age": 30}
print(user["name"]) # Outputs "Alex"

# The following line will cause a KeyError error, since there is no 'city' key.
# print(user["city"])
                        

Getting the value via the get()

methodThe get() method is a secure access method. It does not cause an error if there is no key.

Without default value

If the key is not found, get() returns None by default.

user = {"name": "Alex", "age": 30}
city = user.get("city")
print(f"Name: {user.get('name')}")
print(f"City: {city}") # Outputs None, there will be no error
                        

Specifying the default value

The second argument in get() can be passed the value that will be returned if the key is not found.

user = {"name": "Alex", "age": 30}
# If the key 'city' is not found, return the string 'Not specified'
city = user.get("city", "Not specified")
print(f"City: {city}") # Outputs 'Not specified'
                        

Error handling for accessing a non-existent key

If you use [] but are not sure if the key is available, use the try construct...except.

user = {"name": "Alex", "age": 30}
try:
    city = user["city"]
    print(f"City: {city}")
except KeyError:
    print("The key 'city' was not found in the dictionary.")
                        

Changing and adding elements

Assigning a value by key

The syntax for changing an existing element and adding a new one is exactly the same.

Modification of an existing one

If the key is already in the dictionary, its value will be updated.

user = {"name": "Alex", "age": 30}
print(f"Old age: {user['age']}")
user["age"] = 31 # Updating the value using the key 'age'
print(f"New age: {user['age']}")
                        

Adding a new one

If the key is not in the dictionary, a new key-value pair will be created.

user = {"name": "Alex", "age": 30}
print(f"Dictionary before addition: {user}")
user["email"] = "alex@example.com " # Adding a new
print(f"Dictionary after adding: {user}")
                        

Deleting elements from the dictionary

Method pop()

Deletes the pair by the specified key and returns the deleted value. If the key is not found, it causes a KeyError.

user = {"name": "Alex", "age": 30, "email": "alex@example.com "}
removed_age = user.pop("age")
print(f"Deleted value: {removed_age}")
print(f"Dictionary after deletion: {user}")

# You can specify a default value to avoid the error
removed_city = user.pop("city", "City was not specified")
print(f"Attempt to delete a non-existent key: {removed_city}")
                        

Method popitem()

Deletes and returns the last added pair (key, value) as a tuple. It works according to the LIFO (Last-In, First-Out) principle. If the dictionary is empty, it causes a KeyError.

user = {"name": "Alex", "email": "alex@example.com "}
last_item = user.popitem()
print(f"Deleted pair: {last_item}")
print(f"Dictionary after popitem: {user}")
                        

The del

operatorDeletes a key pair. It doesn't return anything. If the key is not found, it causes a KeyError.

user = {"name": "Alex", "age": 30}
del user["age"]
print(f"Dictionary after del: {user}")
                        

Method clear()

Removes all elements from the dictionary, making it empty.

user = {"name": "Alex", "age": 30}
user.clear()
print(f"Dictionary after clear: {user}")
                        

Python dictionary methods: access and operations

Method get()

As we have already seen, it safely gets the value by the key, returning None or the default value if the key is missing.

Method keys()

Returns a special representation object (dict_keys) containing all the dictionary keys. It can be iterated through in a loop or converted into a list.

car = {"brand": "Ford", "model": "Mustang", "year": 1964}
all_keys = car.keys()
print(f"Key object: {all_keys}")
print(f"Keys in the form of a list: {list(all_keys)}")
                        

Getting values from the dictionary: values()

Returns a representation object (dict_values) with all dictionary values.

car = {"brand": "Ford", "model": "Mustang", "year": 1964}
all_values = car.values()
print(f"Value object: {all_values}")
print(f"List values: {list(all_values)}")
                        

Methods returned by items() dictionary

Returns a representation object (dict_items) with pairs (key, value) in the form of tuples. This is the most convenient way to go through the entire dictionary.

car = {"brand": "Ford", "model": "Mustang", "year": 1964}
all_items = car.items()
print(f"Object pairs: {all_items}")
print(f"Pairs in a list: {list(all_items)}")
                        

Method update()

Updates the dictionary by adding key-value pairs from another dictionary or an iterable object. If the keys match, the values are overwritten.

user_info = {"name": "Alice", "age": 25}
user_contacts = {"email": "alice@email.com ", "age": 26} # age will be updated

user_info.update(user_contacts)
print(user_info)
                        

Method fromkeys()

Creates a new dictionary from the passed key sequence. All keys are assigned the same value (by default None).

keys = ['a', 'b', 'c']
# Create a dictionary with keys from the list and a value of 0 for each
new_dict = dict.fromkeys(keys, 0)
print(new_dict) # {'a': 0, 'b': 0, 'c': 0}

# If no value is specified, None
default_dict  = dict.fromkeys(keys)
print(default_dict) # {'a': None, 'b': None, 'c': None}
                        

Dictionary search

Iterating over keys only

This is the default behavior when iterating through the dictionary.

person = {"name": "Bob", "age": 42, "city": "New York"}
for key in person:
    print(key)
# or explicitly
for key in person.keys():
    print(key)
                        

Sorting only values

The values() method is used for this.

person = {"name": "Bob", "age": 42, "city": "New York"}
for value in person.values():
    print(value)
                        

Sorting key-value pairs using items()

The most common and convenient way. Allows you to immediately get both the key and the value at each iteration.

person = {"name": "Bob", "age": 42, "city": "New York"}
for key, value in person.items():
    print(f"Key: {key}, Value: {value}")
                        

Iteration with conditioning and filtering

Loops can be easily combined with conditional statements if.

products = {"apple": 50, "banana": 20, "orange": 80, "milk": 120}
# Find all products whose price is more than 60
for product, price in products.items():
    if price > 60:
        print(f"{product} costs {price}")
                        

Nested dictionaries in Python

Dictionary structure in the dictionary

The value in the dictionary may be another dictionary. This allows you to create complex, hierarchical data structures.

# User dictionary, where each user is also a dictionary
users = {
    "user1": {
        "name": "Alice",
        "email": "alice@example.com"
    },
    "user2": {
        "name": "Bob",
        "email": "bob@example.com"
    }
}
print(users)
                        

Access to the elements of the nested dictionary

Access is performed via a keychain.

users = {
"user1": { "name": "Alice", "email": "alice@example.com " },
"user2": { "name": "Bob", "email": "bob@example.com " }
}
# Get the email of the user user2
email_bob = users["user2"]["email"]
print(email_bob) # bob@example.com
                        

Changing and adding elements to nested dictionaries

The principle is the same: we get to the right place along the keychain and assign a new value.

users = {
    "user1": { "name": "Alice", "email": "alice@example.com" }
}

# Change the username of user1
users["user1"]["name"] = "Alicia"

# Add age to user user1
users["user1"]["age"] = 30

print(users)
                        

Searching through nested dictionaries

Nested loops are used.

users = {
    "user1": { "name": "Alice", "email": "alice@example.com" },
    "user2": { "name": "Bob", "email": "bob@example.com" }
}

# Go through all the users and their data
for user_id, user_data in users.items():
    print(f"User ID: {user_id}")
    for key, value in user_data.items():
        print(f"{key}: {value}")
                        

Practical tasks

Counting the frequency of characters in a string

is a classic problem, ideally solved using a dictionary.

text = "hello world"
frequency = {}

for char in text:
# Use get() with a default value of 0
    frequency[char] = frequency.get(char, 0) + 1

print(frequency) # {'h': 1, 'e': 1, 'l': 3, 'o': 2, ' ': 1, 'w': 1, 'r': 1, 'd': 1}
                        

Grouping data by common attribute

For example, let's group a list of people by city.

people = [
    {"name": "Alice", "city": "New York"},
    {"name": "Bob", "city": "London"},
    {"name": "Charlie", "city": "New York"},
    {"name": "Diana", "city": "London"}
]

grouped_by_city = {}
for person in people:
    city = person["city"]
# If the city is not already in the dictionary, create an empty list for it.
    if city not in grouped_by_city:
        grouped_by_city[city] = []
    # Adding a person to the list of his city
    grouped_by_city[city].append(person["name"])

print(grouped_by_city) # {'New York': ['Alice', 'Charlie'], 'London': ['Bob', 'Diana']}
                        

Combining two dictionaries

Via update()

The update() method modifies the source dictionary.

dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}

dict1.update(dict2) # dict1 will be changed
print(dict1) # {'a': 1, 'b': 3, 'c': 4}
                        

Via the ** operator

The decompression operator ** (Python 3.5+) allows you to create a new combined dictionary without changing the original ones.

dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}

# If the keys match, the value from the last dictionary will be taken (dict2)
merged_dict = {**dict1, **dict2}
print(merged_dict) # {'a': 1, 'b': 3, 'c': 4}
print(dict1) # {'a': 1, 'b': 2} - remained unchanged
                        

Dictionary inversion

Reverse key and value substitution

A simple case where all values are unique.

original = {'a': 1, 'b': 2, 'c': 3}
inverted = {value: key for key, value in original.items()}
print(inverted) # {1: 'a', 2: 'b', 3: 'c'}
                        

Processing identical values

If the values are not unique, a simple inversion will result in data loss. The correct solution is to group the keys into a list.

original = {'a': 1, 'b': 2, 'c': 1} # The value of '1' repeats
inverted_grouped = {}

for key, value in original.items():
    if value not in inverted_grouped:
        inverted_grouped[value] = []
    inverted_grouped[value].append(key)

print(inverted_grouped) # {1: ['a', 'c'], 2: ['b']}
                        

dict comprehension Dictionary generation

Simple generators: {x: f(x) for x in ...}

Concise syntax for creating dictionaries.

# Dictionary {1: 1, 2: 4, 3: 9, ...}
squares = {x: x * x for x in range(1, 6)}
print(squares)
                        

Generators with condition

You can add if to filter the elements.

# Create a dictionary for odd numbers only
odd_squares = {x: x * x for x in range(1, 10) if x % 2 != 0}
print(odd_squares) # {1: 1, 3: 9, 5: 25, 7: 49, 9: 81}
                        

Generators based on zip() or enumerate()

You can use other functions to get pairs.

# zip
keys = ['name', 'age']
values = ['Tom', 33]
person = {k: v for k, v in zip(keys, values)}
print(person) # {'name': 'Tom', 'age': 33}

# enumerate
items = ['apple', 'banana', 'cherry']
indexed_items = {index: value for index, value in enumerate(items)}
print(indexed_items) # {0: 'apple', 1: 'banana', 2: 'cherry'}
                        

Useful tricks and tricks

Key existence verification via in

The fastest and most "Pythonic" way to check if there is a key in the dictionary.

user = {"name": "Frank", "age": 50}
if "age" in user:
    print("The 'age' key exists.")
if "city" not in user:
    print("The 'city' key is missing.")
                        

Using setdefault()

The setdefault() method is similar to get(), but with one important difference: if the key is missing, it not only returns the default value, but also adds a new pair to the dictionary with this key and the value. This is very convenient for initializing collections.

# Classic example of grouping with setdefault()
data = [("fruits", "apple"), ("vegetables", "carrot"), ("fruits", "banana")]
grouped = {}
for category, item in data:
    # If there is no 'category', a key with the value [] will be created and [] will be returned.
    # If there is, the existing list will simply be returned.
    # In any case, we can do it right away .append()
    grouped.setdefault(category, []).append(item)
print(grouped) # {'fruits': ['apple', 'banana'], 'vegetables': ['carrot']}
                        

Counting and accumulating values

Using dictionaries as counters

These two points are closely related. The dictionary is the perfect tool for counting.

from collections import Counter

# The easy way (already shown)
words = ["apple", "orange", "apple", "banana", "orange", "apple"]
word_counts = {}
for word in words:
    word_counts[word] = word_counts.get(word, 0) + 1
print(word_counts)

# Advanced method using the special class Counter
word_counts_pro = Counter(words)
print(word_counts_pro) # Counter({'apple': 3, 'orange': 2, 'banana': 1})
# Counter is a subclass of dict, it has all the same methods and its own.
print(word_counts_pro.most_common(2)) # [('apple', 3), ('orange', 2)]
                        

Using dictionaries as storage (for example, cache)

Caching (or memoization) is saving the results of a function to prevent repeated calculations with the same arguments.

import time

# Dictionary for cache
fib_cache = {}

def fibonacci(n):
    # If the value is already in the cache, return it
    if n in fib_cache:
        return fib_cache[n]
    
    # Otherwise, calculate
    if n <= 1:
        result = n
    else:
        result = fibonacci(n - 1) + fibonacci(n - 2)
    
    # Save the result to the cache before returning
    fib_cache[n] = result
    return result

start_time = time.time()
print(fibonacci(35))
print(f"Execution time: {time.time() - start_time:.4f} seconds")

# The second call will be almost instantaneous, since all the values are already in the cache
start_time = time.time()
print(fibonacci(35))
print(f"Time of the second call: {time.time() - start_time:.4f} seconds")
                        

Dictionary conversion

Keys/values/pairs in the list

my_dict = {'a': 1, 'b': 2, 'c': 3}
key_list = list(my_dict.keys()) # or just list(my_dict)
value_list = list(my_dict.values())
item_list = list(my_dict.items()) # list of tuples

print(f"Keys: {key_list}")
print(f"Values: {value_list}")
print(f"Pairs: {item_list}")
                        

In JSON (serialization)

JSON (JavaScript Object Notation) is a text data exchange format. The structure of Python dictionaries perfectly matches it. The process of converting to a JSON string is called serialization.

import json

user = {
    "name": "Ivan Petrov",
"age": 30,
"is_active": True,
    "courses": ["Python", "Git"],
    "passport": None
}

# Convert dictionary to JSON string
# ensure_ascii=False to display Cyrillic alphabet correctly
# indent=4 for beautiful formatting with indentation
json_string = json.dumps(user, ensure_ascii=False, indent=4)
print(json_string)

# Reverse transformation (deserialization)
back_to_dict = json.loads(json_string)
print(back_to_dict)
                        

From other structures (for example, list of tuples)

A dictionary can be created from any sequence, the elements of which are themselves sequences of two elements (key, value).

list_of_tuples = [('a', 1), ('b', 2), ('c', 3)]
my_dict = dict(list_of_tuples)
print(my_dict)
                        

Extended structures

Dictionaries with nested lists

A very common structure for storing related collections.

user_permissions = {
    "admin": ["create_user", "delete_user", "edit_content"],
    "editor": ["edit_content"],
    "viewer": ["view_content"]
}

# Check if the admin can delete users
can_delete = "delete_user" in user_permissions["admin"]
print(f"Admin can delete users: {can_delete}")
                        

Dictionaries with sets

Use a set (set) as a value if the uniqueness of the elements and a quick check for occurrence are important to you.

user_tags = {
    "post1": {"python", "web", "django"},
    "post2": {"python", "data-science"},
    "post3": {"web", "css"}
}

# Find all posts with the tag 'python'
python_posts = [post_id for post_id, tags in user_tags.items() if "python" in tags]
print(f"Posts with the tag 'python': {python_posts}")
                        

Dictionaries with functions as values

Functions in Python are first-class objects, they can be stored in dictionaries. This allows you to implement, for example, the "factory" or "dispatcher" pattern.

def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

# A dictionary that maps string operators to functions
operations = {
    "+": add,
    "-": subtract
}

operator = "+"
x, y = 10, 5

# Select the desired function from the dictionary and call it
result =operations[operator](x, y)
print(f"{x} {operator} {y} = {result}")
                        

Common errors and pitfalls

Error KeyError in Python

Occurs when trying to access a non-existent key separated by square brackets []. How to avoid:

  1. Use dict.get().
  2. Check for the key using if key in my_dict.
  3. Use try...except KeyError.

Using mutable objects as keys

Dictionary keys must be immutable and hashable. These are strings, numbers, tuples. Lists (list), sets (set), and other dictionaries (dict) cannot be keys because they are mutable.

valid_dict = {}
valid_dict["string_key"] = 1
valid_dict[123] = 2
valid_dict[(1, 2)] = 3 # A tuple is immutable and can be used as a key.

print(f"Working dictionary: {valid_dict}")

# The following code will cause a TypeError error: unhashable type: 'list'
# invalid_dict = {}
# invalid_dict[[1, 2]] = "won't work"
                        

Shallow vs deep dictionary copying

This is an important concept when working with nested structures.

  • Shallow copying (.copy()): A new dictionary is created, but references to nested objects are copied into it. Changing the nested object in the copy will affect the original as well.
  • Deep copy (copy.deepcopy()): Creates a complete, independent copy, including all nested objects.
import copy

original = {
"a": 1,
"b": [10, 20, 30] # Nested mutable list
}

#surface copy
shallow  = original.copy()
shallow["b"].append(40) # Changing the list in
print(f"Original after changing the shallow copy: {original}") # The original has changed too!

# Deep copy
original = {"a": 1, "b": [10, 20, 30]} # Restoring
deep = copy.deepcopy(original)
deep["b"].append(40) # Changing the list in
print(f"Original after changing the deep copy: {original}") # The original has not changed
                        

Performance and optimization

Dictionary operations

Thanks to hash tables, basic operations (adding, receiving, deleting an element by key) are performed on average in constant time, O(1). This means that the execution time does not depend on the number of items in the dictionary.

Comparison with other data structures

  • Dictionary (search by key): O(1)
  • List (search by value): O(n) - you need to iterate through all the elements.
  • Set (checking for the presence of an element): O(1)

Conclusion: if you need a quick search by a unique identifier, a dictionary is the best choice.

Caching results using dictionaries

As it was shown earlier, dictionaries are an ideal tool for implementing caching (memoization), which is one of the key performance optimization techniques.