Python Dictionaries: Keys, Values, Updates, Loops, and JSON
A Python dictionary stores values under unique keys, so you can look up a name, setting, or count without remembering its position. Use one for records, configuration, and data you need to find by a label.
Think of a mailroom wall with a name on each compartment. You read the label instead of counting slots. If you are still learning loops and variables, the Python learning guide covers those first.
What Is a Python Dictionary?
A Python dictionary is a collection of key and value pairs. The key is the label, the value is whatever sits behind it, and each key appears at most once. Assigning to a key that already exists replaces its value instead of adding a second entry.
user = {"name": "Ada", "email": "ada@example.com", "age": 36}
print(user["name"])
print(len(user)) # Program output:
# Ada
# 3 Values can be anything at all, including lists, other dictionaries, and your own objects. Keys are restricted to hashable objects, which in practice means strings, numbers, booleans, and tuples of those. Lists and dictionaries cannot be keys because they can change after they are made.
Dictionaries also keep insertion order, which the language has guaranteed since Python 3.7. That is the order keys were first added, not alphabetical order, and re-assigning an existing key leaves it where it was.
How Do You Create a Python Dictionary?
Create a Python dictionary with key-value pairs inside braces, or pass pairs to dict(). The constructor also accepts keyword arguments when the keys are valid Python names:
literal = {"host": "localhost", "port": 5432}
keyword = dict(host="localhost", port=5432)
pairs = dict([("host", "localhost"), ("port", 5432)])
empty = {}
print(literal == keyword == pairs)
print(type(empty)) # Program output:
# True
# <class 'dict'> Note that {} is an empty dictionary, not an empty set. That is the one piece of Python punctuation that surprises people coming from other languages, and set() is how you spell the empty set.
Two more routes come up constantly. Pairing two sequences with zip builds a record from headers and a row, and dict.fromkeys builds a dictionary whose keys all start on the same value:
headers = ["name", "age", "city"]
row = ["Ada", "36", "London"]
print(dict(zip(headers, row)))
print(dict.fromkeys(["read", "write"], False)) # Program output:
# {'name': 'Ada', 'age': '36', 'city': 'London'}
# {'read': False, 'write': False} Give fromkeys a mutable default such as a list and every key shares one object, which is the same trap the repeated-empty-list shortcut sets on lists. A dict comprehension is the safe way to give each key its own.
How Do You Read Values From a Python Dictionary?
Read a Python dictionary value with dictionary[key]. Square brackets raise KeyError if the key is missing. The get method asks the same question and hands back None, or a default you supply, instead of raising:
user = {"name": "Ada", "email": "ada@example.com"}
print(user.get("name"))
print(user.get("phone"))
print(user.get("phone", "Not provided")) # Program output:
# Ada
# None
# Not provided When a Missing Key Is a Bug
The choice between the two is not about safety, it is about whose fault a missing key would be. Use brackets when the key must exist and its absence means your program is already wrong, because a loud KeyError at the point of the mistake is worth more than a quiet None three functions later.
Use get when the data comes from outside your control, such as a form field a user may leave blank or an API response with optional fields. There the absence is expected, and a default is a real answer rather than a papered-over failure.
Missing Keys and None Values
Without a distinct default, get() returns None for both a missing key and a key storing None. Use in to check whether the key exists, or supply a unique default value that cannot be confused with stored data:
user = {"phone": None}
print(user.get("phone", "missing"))
print("phone" in user)
print("fax" in user) # Program output:
# None
# True
# False The first line looks wrong until you read it carefully. The default never applies, because the key is present, so get returns the stored None. Reach for in whenever "not set" and "set to nothing" mean different things in your data.
How Do You Change a Python Dictionary?
Change a Python dictionary by assigning a value to a key. A new key adds an entry; an existing key replaces its value. Use pop() or del to remove an entry:
user = {"name": "Ada"}
user["email"] = "ada@example.com"
user["name"] = "Ada Lovelace"
print(user) # Program output:
# {'name': 'Ada Lovelace', 'email': 'ada@example.com'} Removal comes in three forms, matching whether you want the value back and whether you know the key:
user = {"name": "Ada", "age": 36, "city": "London", "role": "admin"}
age = user.pop("age")
last = user.popitem()
del user["name"]
print(age, last)
print(user) # Program output:
# 36 ('role', 'admin')
# {'city': 'London'} The pop method removes by key and returns the value, raising KeyError unless you pass a default as its second argument. The popitem method removes the most recently inserted pair and returns it as a tuple. The del statement removes without returning, and clear() empties the dictionary.
How Do You Merge Two Python Dictionaries?
Merge Python dictionaries with left | right to create a new dictionary, or left.update(right) to change the left dictionary. When both contain the same key, the right dictionary supplies its value. The | operator requires Python 3.9 or later:
defaults = {"host": "localhost", "port": 5432, "debug": False}
overrides = {"debug": True}
merged = defaults | overrides
unpacked = {**defaults, **overrides}
copy_of_defaults = dict(defaults)
copy_of_defaults.update(overrides)
print(merged)
print(defaults) # Program output:
# {'host': 'localhost', 'port': 5432, 'debug': True}
# {'host': 'localhost', 'port': 5432, 'debug': False} The | operator and the ** unpacking both build a new dictionary and leave both inputs alone. The update method writes into the receiver, which is why the example copies first. In every form the right-hand side wins on any key both sides carry.
That last rule is the whole reason the defaults-then-overrides pattern reads so well. Put the settings you shipped on the left and the settings the user chose on the right, and the merge states the precedence in one line.
How Do You Loop Through a Python Dictionary?
Loop through a Python dictionary with for key in dictionary to read its keys, or use dictionary.items() to read each key and value together:
user = {"name": "Ada", "age": 36}
for key in user:
print(key)
for key, value in user.items():
print(key, "=", value) # Program output:
# name
# age
# name = Ada
# age = 36 The three view methods, keys(), values(), and items(), return live views rather than snapshots. Change the dictionary and every view already handed out reflects the change:
user = {"name": "Ada", "age": 36}
labels = user.keys()
user["city"] = "London"
print(labels) # Program output:
# dict_keys(['name', 'age', 'city']) That is useful when you want the view to track the data, and surprising when you expected a frozen copy. Wrap the view in list() when you need one that stops moving.
Counting and Grouping
Dictionaries are useful for counting and grouping. Counting stores how often each item appears. Grouping stores a list of items under each category.
items = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = {}
for item in items:
counts[item] = counts.get(item, 0) + 1
print(counts) # Program output:
# {'apple': 3, 'banana': 2, 'cherry': 1} The get call with a default of 0 is what removes the special case for the first sighting. Without it the loop needs a membership test before every increment.
Setdefault and Its Sharp Edge
Grouping needs a fresh list the first time a category appears. The setdefault method stores the default when the key is absent and returns whatever is now there, so the two steps collapse into one:
people = [("admin", "Ada"), ("user", "Bo"), ("admin", "Cleo")]
by_role = {}
for role, name in people:
by_role.setdefault(role, []).append(name)
print(by_role) # Program output:
# {'admin': ['Ada', 'Cleo'], 'user': ['Bo']} The sharp edge is that the default argument is evaluated before setdefault runs, every single time. Writing cache.setdefault(key, fetch(key)) therefore calls fetch on every pass, including the ones that were meant to be cache hits.
When the fallback is expensive, spell out the membership test instead so the expensive call only happens on a genuine miss.
Defaultdict for the Whole Loop
When missing keys are the expected case rather than the exception, collections.defaultdict moves the bookkeeping into the type. You declare a factory once and then write the loop as if every key already existed:
from collections import defaultdict
people = [("admin", "Ada"), ("user", "Bo"), ("admin", "Cleo")]
by_role = defaultdict(list)
for role, name in people:
by_role[role].append(name)
print(dict(by_role)) # Program output:
# {'admin': ['Ada', 'Cleo'], 'user': ['Bo']} The behavior to remember is that reading a missing key creates it. After by_role["guest"] the guest key exists with an empty list, even though you only looked. Use get when you want to inspect without writing.
How Do You Sort a Python Dictionary?
Sort a Python dictionary by passing sorted key-value pairs to dict(). Dictionaries have no in-place sort() method; the new dictionary keeps the order of the pairs you supply:
prices = {"cherry": 2.0, "apple": 1.5, "banana": 0.75}
by_key = dict(sorted(prices.items()))
by_value = dict(sorted(prices.items(), key=lambda pair: pair[1]))
print(by_key)
print(by_value) # Program output:
# {'apple': 1.5, 'banana': 0.75, 'cherry': 2.0}
# {'banana': 0.75, 'apple': 1.5, 'cherry': 2.0} The key function says which half of each pair to compare, so pair[1] sorts on values. Add reverse=True for descending order. When only the top few matter, heapq.nlargest avoids sorting the whole thing.
Nested Dictionaries and JSON
Real data nests. An API response holds a user, which holds an address, which holds a ZIP code, and each level is another dictionary. Chained get calls read a deep path without raising on any missing key:
response = {"data": {"user": {"profile": {"bio": "Engineer"}}}}
bio = response.get("data", {}).get("user", {}).get("profile", {}).get("bio", "No bio")
missing = response.get("data", {}).get("account", {}).get("plan", "free")
print(bio)
print(missing) # Program output:
# Engineer
# free Each {} default keeps the chain going when a key is absent. It does not guard a key that is present with the value None: {"data": None} returns None, and the next get raises AttributeError. Validate levels that can be null before chaining.
Past three levels the line stops being readable either way, and naming the intermediate shape in its own variable is usually the better answer.
The json module is the boundary between dictionaries and text. It maps a dictionary to a JSON object and back, with a few conversions worth knowing:
import json
record = {"name": "Ada", "active": True, "score": None, 2026: "year"}
text = json.dumps(record)
back = json.loads(text)
print(text)
print(list(back.keys())) # Program output:
# {"name": "Ada", "active": true, "score": null, "2026": "year"}
# ['name', 'active', 'score', '2026'] Three conversions happen there. Python's True becomes true, None becomes null, and the integer key becomes a string because JSON objects only allow string keys. That last one means a round trip does not always give you back the dictionary you started with.
Types with no JSON equivalent, such as datetime and set, raise TypeError on json.dumps. Convert them yourself before encoding, usually to an ISO date string and a list.
A Worked Example: Summarizing Orders
A list of order records has to become a per-customer total and a list of what each one bought, which is counting and grouping in the same pass.
orders = [
{"customer": "Ada", "item": "keyboard", "total": 80.0},
{"customer": "Bo", "item": "monitor", "total": 210.0},
{"customer": "Ada", "item": "mouse", "total": 25.0},
]
spend = {}
bought = {}
for order in orders:
who = order["customer"]
spend[who] = spend.get(who, 0) + order["total"]
bought.setdefault(who, []).append(order["item"])
print(spend)
print(bought) # Program output:
# {'Ada': 105.0, 'Bo': 210.0}
# {'Ada': ['keyboard', 'mouse'], 'Bo': ['monitor']} Note the two access styles doing different jobs in the same loop. Reading order["customer"] uses brackets because a record without a customer is broken data and should fail loudly. Reading spend.get(who, 0) uses a default because a first-time customer is entirely normal.
Ranking the result is the sort pattern from earlier, applied to the summary rather than to the raw orders:
spend = {"Ada": 105.0, "Bo": 210.0}
ranked = dict(sorted(spend.items(), key=lambda pair: pair[1], reverse=True))
print(ranked) # Program output:
# {'Bo': 210.0, 'Ada': 105.0} Pitfalls and Debugging
KeyError on Data You Do Not Control
The error KeyError: 'phone' means the dictionary has no phone key. Check for a misspelled key or a field missing from the input before choosing a fallback.
Fix it at the boundary rather than at the crash site. Decide once, where the data enters your program, which fields are required and which have defaults, and apply get or a validation step there.
Unhashable Key Types
Using a list as a key raises TypeError: unhashable type: 'list'. A key has to be stable, because the dictionary files it by its hash, and a list can change after filing:
prices = {}
prices[("London", "SW1")] = 42
print(prices) # Program output:
# {('London', 'SW1'): 42} A tuple is the fix when you want a compound key, as long as everything inside it is hashable too. A tuple containing a list is not hashable and raises the same error.
Changing Keys While Looping
Adding or deleting keys while iterating is invalid and may raise RuntimeError: dictionary changed size during iteration or skip entries. Changing the values behind existing keys is allowed, because the set of keys is what the loop is walking.
counts = {"apple": 3, "banana": 2}
for key in counts:
counts[key] = counts[key] * 2
print(counts)
for key in list(counts):
if counts[key] > 5:
del counts[key]
print(counts) # Program output:
# {'apple': 6, 'banana': 4}
# {'banana': 4} Wrapping the dictionary in list() takes a snapshot of the keys, so the loop walks a fixed sequence while the dictionary underneath it changes.
A Shallow Copy That Shares Its Insides
The copy() method and dict(original) both copy the outer dictionary only. Nested dictionaries and lists are still shared, so a change through one copy shows up in the other:
import copy
settings = {"window": {"width": 800}}
shallow = settings.copy()
deep = copy.deepcopy(settings)
shallow["window"]["width"] = 1024
print(settings["window"]["width"])
print(deep["window"]["width"]) # Program output:
# 1024
# 800 Use copy.deepcopy when the nested objects need to be independent, and remember it walks the whole structure, so it is not free on large data.
When Should You Use Something Other Than a Dictionary?
Use a class or dataclass for records with fixed fields, a set when you only need unique values, or a list when positions matter more than names. A dictionary fits data you want to look up by key.
- When the keys are always the same handful of names, a class or a
dataclassgives you attribute access, a readable repr, and an error when you try to read a field that does not exist. - When you only ever ask whether something is present, a set stores the labels without the empty pigeonholes behind them.
- When order and position are the point, a list says so, and a dictionary keyed by
0,1, and2is a list wearing a disguise.
Our comparison of lists, dictionaries, and sets shows when each collection fits.
Frequently Asked Questions
Are Python dictionaries ordered?
Python dictionaries preserve insertion order, a language guarantee since Python 3.7. Keys appear in the order they were added, not sorted order. Assigning a new value to an existing key leaves its position unchanged.
What can you use as a Python dictionary key?
A Python dictionary key must be hashable. Strings, numbers, and tuples containing only hashable objects work. Lists, dictionaries, and sets are unhashable, so using one as a key raises TypeError.
Can two Python dictionary keys hold the same value?
Two or more Python dictionary keys can hold the same value. Only the keys must be unique. Assigning to an existing key replaces its value instead of adding a second entry.
How do you check whether a Python dictionary key exists?
Use key in dictionary to check whether a Python dictionary contains a key. Calling get() without a distinct default cannot tell a missing key apart from a key storing None. If you also need the value, pass a unique sentinel object as the default.
How do Python dict.pop() and dict.popitem() differ?
Python dict.pop() removes a specified key and returns its value. dict.popitem() removes the last inserted entry and returns a key-value tuple. pop() accepts a default for a missing key; popitem() raises KeyError when the dictionary is empty.
Are Python dictionaries thread-safe?
A Python dictionary does not make a sequence of reads and writes thread-safe. Another thread can change the data between those steps. Protect shared multi-step updates with a lock rather than assuming interpreter protections make the whole operation atomic.
How do you turn two Python lists into a dictionary?
Use dict(zip(keys, values)) to turn two Python lists into a dictionary, pairing items by position. By default, zip() stops at the shorter list, so check the lengths when every item must be paired. Repeated keys keep the last paired value.
Why does JSON turn Python dictionary integer keys into strings?
JSON objects only allow string keys, so Python's json.dumps() converts integer dictionary keys to strings. Loading that JSON keeps the string keys. A dictionary with integer keys therefore changes when you serialize it to JSON and load it again.
Self-Check
- What does
{}create, and how do you spell an empty set? - Given
user = {"phone": None}, what doesuser.get("phone", "missing")return, and why? - Which of
updateand|changes the dictionary on the left? - Why does
cache.setdefault(key, fetch(key))callfetchon every pass? - What happens to a
defaultdict(list)when you merely read a key that is not there? - Why can a tuple be a dictionary key when a list cannot?
- After
json.loads(json.dumps({1: "a"})), what is the key, and why?
Answers
- An empty dictionary. The empty set has no literal spelling, so it is written
set(). None. The key is present, so the default never applies and the stored value comes back. Use"phone" in userto test presence.update. It writes into the receiver, while|builds a new dictionary and leaves both inputs alone.- Because arguments are evaluated before the call. Python computes
fetch(key)first, then passes the result in, so a cache hit still pays for the fetch. - The key is created with an empty list. Reading a missing key runs the factory and stores the result, so use
getwhen you want to look without writing. - A tuple is hashable only when every element is hashable. Lists do not support hashing, so neither a list nor a tuple containing one can be a dictionary key.
- The string
"1". JSON objects only allow string keys, so the integer key is converted on the way out and stays a string on the way back.
Where to Go Next
Practice choosing between brackets for required keys and get() for optional values. Then try the counting and grouping examples with your own records.
Sources
-
[1]
Data Structures: Dictionaries(docs.python.org)
-
[2]
Mapping Types: dict(docs.python.org)
-
[3]
collections: defaultdict objects(docs.python.org)
-
[4]
json: JSON encoder and decoder(docs.python.org)
Read Next
Three built-in collections answer three different questions: what came in what order, what is stored under this name, and have I seen this before. A criteria-based way to pick one, with the cost of picking wrong.
A comprehension builds a list or a dictionary from an existing iterable in one expression. The syntax for both forms, filtering, conditional values, nesting, and the cases where a plain loop is the better answer.
What is Python used for, and who uses it? Is it slow, how do AI agents change learning it, Python or JavaScript first, and a roadmap to every Python guide.