Learn

Learn

Python Dictionaries — The Basics

Build a real mental model of Python dictionaries: key-value pairs, safe lookup with get, why a dict is shared by reference, iterating with items, dict comprehensions, and reaching into nested data.

XCODXLearn · Updated

Introduction

A dictionary (or dict) is a collection of labelled values. Where a list is a numbered row of items, a dictionary is a set of named slots: each stored value is filed under a key, and together a key and its value form one entry. Dictionaries are how you model a *thing* described by named fields — a user with a name and an email, a product with a price and a title, a config with a dozen options looked up by name.

Why a second collection type alongside lists? Because order and naming solve different problems. Reach for a list when you have a *sequence* of similar items and position matters; reach for a dictionary when you have *one thing* described by fields you will look up by name. Real programs weave both together: lists of dicts, dicts containing lists. If you already know Python lists, a dict is the naming counterpart.

Two ideas make dictionaries predictable, and this guide is built around them. First, you read and write each value by its key, either with square brackets or with the safer get method. Second, a dictionary variable holds a reference to the dict, not a private copy — the source of the most common surprises when you think you duplicated your data. Hold those two ideas and the rest is detail.

user = {
    "name": "Ada",   # key "name" -> value "Ada"
    "age": 36,        # key "age"  -> value 36
}

Lessons

1. Key-value pairs: the foundation

Every entry in a dictionary pairs a key with a value. Keys must be unique and hashable — strings and numbers are the everyday choice — while the values can be anything at all, including lists and other dictionaries. You write a dict with curly braces, listing key: value pairs separated by commas. That is the whole foundation: a dictionary *is* its set of key-value pairs, and everything else is a way to read, add, or remove them.

book = {
    "title": "Dune",
    "pages": 412,
    "in_stock": True,
}

2. Reading a value: brackets vs get

There are two ways to read a value by key. Square-bracket access (d["title"]) is direct and clear, but it *raises* a KeyError the moment the key is missing — which crashes the program if you are not certain the key exists. The get method (d.get("title")) is the safe alternative: it returns the value when the key is present and None (or a default you supply) when it is not, so it never raises. Prefer brackets when the key is guaranteed, and get when it might be absent.

book = {"title": "Dune", "pages": 412}

print(book["title"])              # Dune  (bracket - known key)
print(book.get("author"))         # None  (get - missing, no crash)
print(book.get("author", "n/a"))  # n/a   (get with a default)

See the dict.get() reference for the full rules on its default argument. Using get is the single habit that removes most KeyError crashes from real code.

3. Dictionaries are mutable and shared by reference

A dictionary variable does not hold the dict itself; it holds a reference to it. Assigning one dict variable to another copies the reference, so both names point at the *same* dictionary and a change through one name is visible through the other. This is the biggest source of dict bugs: you thought you had a separate copy, edited it, and the original changed too.

original = {"count": 1}
alias = original      # same dict, second name
alias["count"] = 2

print(original["count"])   # 2 - changed through 'alias'
print(alias is original)   # True - one dict, two references

To get an independent dictionary, make a copy with dict(original) or {original}. Both are shallow** copies: the top level is new, but any nested list or dict inside is still shared. For fully independent nested data, reach for copy.deepcopy(original).

copy = {**original}
copy["count"] = 9
print(original["count"])   # unchanged by edits to 'copy'

4. Iterating with items, keys and values

To loop over a dictionary you choose *what* you want to see. Iterating the dict directly gives you the keys, .values() gives just the values, and .items() gives (key, value) pairs — perfect for unpacking in a for loop when you need both halves at once. These three views are the workhorses of dictionary processing.

prices = {"pen": 3, "book": 12}

for name, cost in prices.items():
    print(name, "costs", cost)
# pen costs 3
# book costs 12

The dict.items() reference covers the view object these methods return and why it stays in sync with the dict. When a loop needs the key and the value together, .items() is almost always the right starting point.

5. Building dicts with a comprehension

A dict comprehension builds a new dictionary from an iterable in one expression, mirroring the list comprehension you may already know. The shape is {key_expr: value_expr for item in iterable}, with an optional if filter. It is the concise, idiomatic way to transform or invert data without an explicit loop and a growing dict.

words = ["hi", "hey", "hello"]

lengths = {w: len(w) for w in words}
print(lengths)   # {'hi': 2, 'hey': 3, 'hello': 5}

6. Nested dictionaries and safe access

Real data is nested: a dictionary inside a dictionary inside a list. Reaching deep with plain bracket access raises the moment a link in the chain is missing. Chaining get with a default at each level keeps the read safe — each step falls back to an empty dict rather than crashing, so the final lookup always has something to ask.

data = {"user": {"profile": {"city": "Cairo"}}}

city = data.get("user", {}).get("profile", {}).get("city")
plan = data.get("user", {}).get("account", {}).get("plan", "free")
# city -> 'Cairo', plan -> 'free' (no KeyError)

That is the whole model. A dictionary is a set of key-value pairs; you read each value by key with brackets or the safer get; the variable holds a reference, so copy with dict(d) or {**d} before editing; iterate with .items(), .keys() or .values() depending on what you need; build new dicts with a comprehension; and reach into nested data safely by chaining get. Every dictionary method beyond these rests on that same frame.

Practice

Check your model. After b = a where a = {"n": 1}, what does b["n"] = 5 do to a["n"]? (Answer: it becomes 5 too — a and b are two names for one dictionary.) Now confirm that a copy breaks the link before you edit it:

user = {"name": "Ada", "roles": ["admin"]}

copy = dict(user)
copy["name"] = "Bo"

print(user["name"])   # Ada - top-level copy is independent
print(copy["name"])   # Bo
print(list(user.items()))   # [('name', 'Ada'), ('roles', ['admin'])]

Next steps

You have the mental model — now put it to work. Browse runnable Python dictionary examples for copy-paste snippets, follow how to merge dictionaries for a common task, or read the references for dict.get() and dict.items(). If you are comparing collections, revisit Python lists — the basics. Every snippet runs in the online Python compiler or the full editor.

Frequently asked questions

What is the difference between d[key] and d.get(key)?
Both read a value by key. d[key] raises a KeyError when the key is missing, while d.get(key) returns None (or a default you pass) instead of raising, so it is the safe choice when the key might be absent.
Why did changing my copied dictionary also change the original?
Assigning with = copies the reference, not the dictionary — both names point at the same dict. Copy it first with dict(d) or {**d} (shallow) or copy.deepcopy(d) (deep) before editing.
How do I loop over a dictionary's keys and values together?
Use for key, value in d.items():. The items view yields (key, value) pairs you can unpack directly. Use d.keys() or d.values() when you need only one side.
How do I safely read a value from a nested dictionary?
Chain get with an empty-dict default at each level: d.get("a", {}).get("b", {}).get("c"). Each step falls back instead of raising a KeyError if a link is missing.