Dictionaries in Python (dict): Creating, Methods, Looping, Sorting and frozendict
A dictionary in Python (dict) is a mutable collection of key–value pairs that finds a value by its key almost instantly; it is created with curly braces, for example {"coffee": 250}, or with the dict() function. Keys are unique and must be hashable; since Python 3.7 insertion order is preserved, and Python 3.15 added a built-in immutable variant — frozendict.
A dictionary in Python (dict) is a collection of key–value pairs in which a value is found by its key almost instantly, no matter how many records there are. If you need to total expenses by category, store settings or quickly look up a user by id, you use a dictionary. Below is everything you need to work with a Python dict: creating it, methods, looping, sorting, merging and the novelty of 3.15 — frozendict.
What a dictionary is in Python
A dictionary stores pairs where the key is unique: if you write a value under an existing key, the old one is replaced. Internally it is a hash table, so searching, adding and deleting by key work in constant time on average, not by scanning.
A key must be a hashable object: a string, a number, a tuple of immutable values. A list cannot be a key — you will get TypeError: unhashable type: 'list'. Values, however, can be anything, including other dictionaries and lists.
Since Python 3.7 a dictionary is guaranteed to remember insertion order: when you iterate over it, you get the keys in the order they were added.
How to create a dictionary
There are several ways to create a dictionary, and all of them appear in real code.
spent = {"Food": 1200, "Taxi": 450} # a literal
empty = {} # an empty dictionary
also_empty = dict() # the same, via the dict function
user = dict(name="Anna", age=30) # keyword arguments
pairs = dict([("a", 1), ("b", 2)]) # from a list of pairs
keys = dict.fromkeys(["x", "y"], 0) # {'x': 0, 'y': 0}
Note that empty curly braces {} create an empty dictionary, not a set. An empty set is made only with set().
A convenient way to build a dictionary from two lists is zip:
names = ["Food", "Taxi", "Cinema"]
sums = [1200, 450, 600]
spent = dict(zip(names, sums))
print(spent) # {'Food': 1200, 'Taxi': 450, 'Cinema': 600}
Access by key: d[k] and get
A value is read with square brackets. If the key is missing, Python raises KeyError. To avoid the error, use the get method: it returns None or a default value.
spent = {"Food": 1200}
print(spent["Food"]) # 1200
print(spent.get("Cinema")) # None
print(spent.get("Cinema", 0)) # 0
spent["Cinema"] # KeyError: 'Cinema'
To check whether a key exists, use the in operator: "Food" in spent returns True. It checks keys, not values. For values write 1200 in spent.values().
Adding, changing and deleting
Adding or updating an entry is a single assignment line. For counting, the combination with get is very convenient:
spent = {}
for category, amount in [("Food", 250), ("Taxi", 300), ("Food", 400)]:
spent[category] = spent.get(category, 0) + amount
print(spent) # {'Food': 650, 'Taxi': 300}
You can delete a key with del d[k] (it raises KeyError if the key is missing) or with d.pop(k, default), which also returns the value. To empty a dictionary completely, use d.clear(). The length of a dictionary, that is the number of pairs, is len(d).
Dictionary methods: a cheat sheet
| Method | What it does |
|---|---|
get(k, default) | the value for the key, or default, without an error |
setdefault(k, default) | returns the value; if the key is missing, first writes default |
update(other) | adds and overwrites pairs from another dictionary |
pop(k, default) | removes the key and returns the value |
popitem() | removes and returns the most recently added pair |
keys(), values(), items() | views of keys, values and pairs |
copy() | shallow copy |
clear() | empty the dictionary |
fromkeys(keys, value) | a new dictionary with one shared value |
setdefault: filling a dictionary with lists
The setdefault method helps when the values are lists and you need to group items:
groups = {}
for item, cat in [("coffee", "Food"), ("metro", "Transport"), ("pizza", "Food")]:
groups.setdefault(cat, []).append(item)
print(groups) # {'Food': ['coffee', 'pizza'], 'Transport': ['metro']}
update: updating a dictionary
update changes a dictionary in place: new keys are added, matching keys are overwritten.
cfg = {"debug": False, "lang": "en"}
cfg.update({"debug": True, "theme": "dark"})
print(cfg) # {'debug': True, 'lang': 'en', 'theme': 'dark'}
defaultdict: a dictionary with default values
collections.defaultdict is a subclass of dict that, when a missing key is accessed, creates the value itself using a factory. Counting and grouping code becomes shorter.
from collections import defaultdict
spent = defaultdict(int)
for cat, amount in [("Food", 250), ("Taxi", 300), ("Food", 400)]:
spent[cat] += amount
print(dict(spent)) # {'Food': 650, 'Taxi': 300}
groups = defaultdict(list)
groups["Food"].append("coffee")
For simple counting of occurrences there is also collections.Counter: Counter(["a", "b", "a"]) gives Counter({'a': 2, 'b': 1}).
Iterating over a dictionary: the for loop
A for loop over a dictionary goes over the keys by default. To get the key and the value together, iterate over items() — this is the most common variant.
spent = {"Food": 650, "Taxi": 300}
for cat in spent: # keys
print(cat)
for amount in spent.values(): # values
print(amount)
for cat, amount in spent.items(): # pairs
print(f"{cat}: {amount}")
You cannot change the size of a dictionary during iteration — you will get RuntimeError: dictionary changed size during iteration. If you need to delete some keys, iterate over a copy: for k in list(d):.
Printing a dictionary
print(d) prints the dictionary on one line. For a large or nested dictionary, the pprint module or json.dumps(d, ensure_ascii=False, indent=2) is more convenient — it prints the data with indentation, and non-Latin characters without escaping.
Sorting a dictionary
A dictionary itself is not sorted in place: it has no sort method. Sorting a dictionary means creating a new dictionary from the sorted pairs using sorted.
spent = {"Taxi": 300, "Food": 650, "Cinema": 120}
by_key = dict(sorted(spent.items()))
print(by_key) # {'Cinema': 120, 'Food': 650, 'Taxi': 300}
by_value = dict(sorted(spent.items(), key=lambda p: p[1], reverse=True))
print(by_value) # {'Food': 650, 'Taxi': 300, 'Cinema': 120}
If you only need the top N categories, do not convert the result back into a dictionary: sorted(spent.items(), key=lambda p: p[1], reverse=True)[:2] returns a list of pairs.
To sort a list of dictionaries by a field, use: sorted(users, key=lambda u: u["age"]).
Merging and combining dictionaries
Since Python 3.9, dictionaries are merged with the | operator, and |= updates a dictionary in place. When keys clash, the right operand wins.
a = {"x": 1, "y": 2}
b = {"y": 20, "z": 3}
print(a | b) # {'x': 1, 'y': 20, 'z': 3}
print({**a, **b}) # the same via unpacking
a |= b # the same as a.update(b)
Note that a + b does not work for dictionaries: it raises TypeError. If you really want to add the values for identical keys, use Counter: Counter(a) + Counter(b).
Unpacking a dictionary
Two asterisks ** unpack a dictionary into named arguments of a function or into another dictionary:
def greet(name, city):
return f"{name} from {city}"
data = {"name": "Anna", "city": "Kazan"}
print(greet(**data)) # Anna from Kazan
Dictionary comprehension
A dictionary comprehension builds a dictionary in one line from any sequence:
squares = {n: n * n for n in range(5)}
print(squares) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
big = {k: v for k, v in spent.items() if v > 200}
inverted = {v: k for k, v in {"a": 1, "b": 2}.items()}
print(inverted) # {1: 'a', 2: 'b'}
The last example shows how to invert a dictionary by swapping keys and values. It works if the values are unique and hashable; otherwise some pairs will be lost.
Nested dictionaries
A value can be another dictionary — this is how you describe structures such as JSON responses from APIs, or a dictionary of user dictionaries.
users = {
101: {"name": "Anna", "skills": ["python", "sql"]},
102: {"name": "Oleg", "skills": ["go"]},
}
print(users[101]["skills"][0]) # python
print(users.get(103, {}).get("name")) # None, without KeyError
Copying a dictionary: a copy and a second name
The statement b = a does not copy a dictionary: it attaches a second name to the same object. Change it through b and you will see the change in a. The same happens when a dictionary is passed to a function: the function gets a reference to the shared object and can quietly spoil it.
import copy
a = {"limits": [0, 500]}
b = a.copy() # shallow copy
b["limits"].append(1000) # the nested list is shared!
print(a) # {'limits': [0, 500, 1000]}
c = copy.deepcopy(a) # a fully independent copy
Comparing dictionaries
The == operator compares dictionaries by content: they are equal if all pairs match. The order of keys is not taken into account: {"a": 1, "b": 2} == {"b": 2, "a": 1} gives True. To find common keys, use set operations on the views: a.keys() & b.keys().
Typed dictionaries: annotations
A dictionary annotation is written as dict[str, int]. If a dictionary has a fixed set of fields of different types, the TypedDict class from the typing module is convenient:
from typing import TypedDict
class User(TypedDict):
name: str
age: int
u: User = {"name": "Anna", "age": 30}
Annotations are not checked at run time; they are read by IDEs and analysers such as mypy.
An immutable dictionary: frozendict in Python 3.15
Python 3.15 introduced the built-in type frozendict (PEP 814): no import is needed, and an attempt to change it immediately ends with a TypeError. Previously an immutable dictionary had to be imitated with a wrapper from the types module or with a third-party package. This is exactly what is needed for shared settings that no function should quietly change.
config = frozendict(min_amount=0, max_amount=500)
config["max_amount"] = 1000
# TypeError: 'frozendict' object does not support item assignment
strict = config | {"max_amount": 300} # a new frozendict, the original is intact
cache = {config: "report for May"} # hashable — works as a key
What to know, according to the course materials:
frozendictinherits fromobject, not fromdict, soisinstance(cfg, dict)returnsFalse. To check for "any dictionary", usecollections.abc.Mappingorisinstance(x, (dict, frozendict)).- It is hashable only if its keys and values are hashable:
frozendict(a=[1])can be created, buthash()will fail. The immutability is shallow, like that of a tuple. - The order is kept, but
==does not take it into account. json.dumps,pickleandcopyunderstand frozendict.
Common mistakes
- KeyError when reading — use
getor anincheck. - A list as a key — convert it to a tuple:
d[tuple(lst)]. - Changing a dictionary inside a loop — iterate over
list(d). - Expecting a copy from an assignment — use
copy()ordeepcopy. {}instead of an empty set — it is an empty dictionary.
Summary
The Python dict is the language's main data structure: fast lookup by key, preserved insertion order, convenient methods get, setdefault, update, iteration through items(), sorting through sorted and merging with the | operator. Remember about copying and hashable keys, and for settings in Python 3.15 use frozendict. Dictionaries, lists, tuples and sets are practised hands-on in our course "Python 3.15: from beginner to pro", using an expense tracker as the example.
FAQ
Can a dictionary hold values of different types?
Yes, values can be anything and different within one dictionary: numbers, strings, lists, other dictionaries. The only restriction applies to keys — they must be hashable.
How do I find out how many items a dictionary has?
The function len(d) returns the number of key–value pairs. For an empty dictionary it returns 0, and an empty dictionary counts as false in an if condition.
How does dict differ from defaultdict?
defaultdict is a subclass of dict that, instead of raising KeyError on a missing key, creates a value through a factory such as int or list. In every other respect it behaves like an ordinary dictionary.
How do I get a key by its value?
There is no direct method; you need a search: next((k for k, v in d.items() if v == value), None). If you search this way often, it is simpler to build a reverse dictionary with a comprehension {v: k for k, v in d.items()}.
How do I save a dictionary to a file?
Most often through the json module: json.dump(d, f, ensure_ascii=False, indent=2) into a file opened for writing, and json.load(f) to read it back. The keys become strings in the process.
This topic is covered in depth with practice in the course “Python 3.15: от новичка до профи”.
Go to course