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:
- You provide a key (for example, the word "apple").
- Python calculates a hash from this key — a unique numeric representation.
- 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:
- Use
dict.get(). - Check for the key using
if key in my_dict. - 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.