Python Dictionaries: The Complete Beginner's Guide

Lists are positional. Dictionaries are labeled. This chapter covers key-value access, safe missing-key patterns, \`items()\` iteration, nested dictionaries, comprehensions, and practical merge/update techniques used in real API and config code.

Chapter 7 of 20 · Beginner · 35 min · Python Programming Course

Lists store items by position. You access them by index — items[0], items[3]. That works well when position has meaning. But most real data is not positional — it is labelled. A user has a name, an age, an email. A product has a price, a category, a stock count. A server response has a status code, a message, a payload.

When data has labels, dictionaries are the right tool. A dictionary stores key-value pairs — instead of accessing data by its position, you access it by a meaningful name you choose. user["name"] is clearer than user[0]. product["price"] is safer than hoping price is always the third item in a list.

Dictionaries are the backbone of most real Python programs. JSON APIs return dictionaries. Configuration files become dictionaries. Database rows become dictionaries. Once you understand this data structure deeply, a large portion of real-world Python code becomes readable.

What You Will Learn in This Chapter

By the end of this tutorial you will be able to:

  • Create dictionaries using literals and the dict() constructor
  • Access values using bracket notation and get()
  • Add, update, and delete key-value pairs
  • Use all essential dictionary methods: keys(), values(), items(), get(), pop(), update(), setdefault(), copy(), and clear()
  • Iterate over dictionaries using loops
  • Handle missing keys safely without crashing
  • Build nested dictionaries for structured data
  • Write dictionary comprehensions
  • Merge dictionaries using multiple approaches
  • Avoid the most common dictionary mistakes beginners make

Estimated time: 35 minutes reading + 20 minutes practice

Keep a REPL or editor open while reading. Dictionaries become intuitive only when you type the examples, mutate keys, inspect items() output, and intentionally trigger a few KeyError cases to see when get() is the safer choice.

One mental model helps everywhere: a list answers “what is at position i?”, while a dictionary answers “what value belongs to this label?” Once that distinction clicks, JSON payloads, API responses, and configuration objects stop feeling noisy and start feeling structured.

What Is a Python Dictionary?

A dictionary is an ordered, mutable collection of key-value pairs. Every value is stored with a unique key that you use to retrieve it — like looking up a word in a physical dictionary to find its definition.

person = {
    "name": "Elena",
    "age": 30,
    "city": "Mumbai",
    "is_enrolled": True
}

print(person)
# {'name': 'Elena', 'age': 30, 'city': 'Mumbai', 'is_enrolled': True}

Four properties to understand immediately:

  • Keys must be unique. If you use the same key twice, the second value silently overwrites the first. No error, no warning.
  • Keys must be hashable. Strings, integers, floats, and tuples of hashable items can be keys. Lists and other dicts cannot — they are mutable and therefore not hashable.
  • Values can be anything. Strings, numbers, booleans, lists, other dictionaries, functions — any Python object can be a dictionary value.
  • Ordered as of Python 3.7. Dictionaries maintain insertion order. When you iterate or print a dictionary, items appear in the order they were added.

Creating Dictionaries

Dictionary Literals — Curly Braces With Colons

# Empty dictionary
empty = {}

# String keys — most common
student = {
    "name": "Arjun",
    "age": 21,
    "gpa": 3.8,
    "courses": ["Python", "SQL", "Statistics"]
}

# Integer keys
squares = {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# Mixed key types (valid but uncommon)
mixed_keys = {"name": "Alice", 1: "one", (0, 0): "origin"}

The dict() Constructor

# From keyword arguments — keys must be valid Python identifiers
person = dict(name="Bob", age=25, city="Delhi")
print(person)    # {'name': 'Bob', 'age': 25, 'city': 'Delhi'}

# From a list of key-value pairs
pairs = [("name", "Carol"), ("age", 28), ("city", "Chennai")]
person = dict(pairs)
print(person)    # {'name': 'Carol', 'age': 28, 'city': 'Chennai'}

# From two separate lists using zip()
keys = ["name", "age", "city"]
values = ["David", 32, "Pune"]
person = dict(zip(keys, values))
print(person)    # {'name': 'David', 'age': 32, 'city': 'Pune'}

dict(zip(keys, values)) is a pattern you will use constantly when building dictionaries from data sources — CSV columns, API fields, database rows.

Accessing Dictionary Values

Bracket Notation — Direct Access

student = {"name": "Priya", "age": 20, "gpa": 3.9}

print(student["name"])    # Priya
print(student["gpa"])     # 3.9

Bracket notation raises a KeyError if the key does not exist:

# print(student["email"])
# KeyError: 'email'

get() — Safe Access With a Default

get() returns the value if the key exists, or a default value if it does not — without raising an error:

student = {"name": "Priya", "age": 20}

print(student.get("name"))           # Priya
print(student.get("email"))          # None — key missing, no error
print(student.get("email", "N/A"))   # N/A — custom default
print(student.get("age", 0))         # 20  — key exists, returns value

When to use get() vs brackets:

Use bracket notation when the key must exist and its absence is a bug — the KeyError tells you something went wrong. Use get() when the key might legitimately be absent and you have a sensible default.

# API response — field might not always be present
response = {"status": 200, "data": {"user": "alice"}}

username = response.get("data", {}).get("user", "anonymous")
print(username)    # alice

error_msg = response.get("error", "No error")
print(error_msg)   # No error

Checking Key Existence

Always use in to check whether a key exists before accessing it:

student = {"name": "Priya", "age": 20}

print("name" in student)      # True
print("email" in student)     # False
print("email" not in student) # True

# Safe access pattern
if "email" in student:
    print(student["email"])
else:
    print("No email on record")

in checks keys by default. To check values, use in student.values():

print(20 in student.values())      # True
print("Priya" in student.values()) # True
print("Bob" in student.values())   # False

Modifying Dictionaries

Adding and Updating Items

Assignment adds a new key if it does not exist, or updates the value if it does:

student = {"name": "Arjun", "age": 21}

# Add new key
student["email"] = "arjun@example.com"
student["gpa"] = 3.7
print(student)

# Update existing key
student["age"] = 22
student["gpa"] = 3.9
print(student)

update() — Add or Update Multiple Keys at Once

student = {"name": "Arjun", "age": 21}

student.update({"age": 22, "email": "arjun@example.com", "city": "Delhi"})
print(student)

# update() also accepts keyword arguments
student.update(gpa=3.9, is_enrolled=True)
print(student)

update() is the cleanest way to apply multiple changes at once — common when merging configuration data or applying API response fields to a local record.

setdefault() — Add a Key Only if It Does Not Exist

student = {"name": "Arjun", "age": 21}

# Adds "email" with default value because key is missing
student.setdefault("email", "not_provided@example.com")
print(student["email"])    # not_provided@example.com

# Does NOT overwrite because "name" already exists
student.setdefault("name", "Unknown")
print(student["name"])     # Arjun — unchanged

setdefault() is useful for initialising nested structures safely without overwriting existing data:

# Safely initialise a list for a new key
scores = {}
scores.setdefault("Alice", []).append(88)
scores.setdefault("Alice", []).append(92)
scores.setdefault("Bob", []).append(75)
print(scores)    # {'Alice': [88, 92], 'Bob': [75]}

Removing Items From Dictionaries

pop() — Remove by Key and Return the Value

student = {"name": "Arjun", "age": 21, "gpa": 3.7, "city": "Delhi"}

removed = student.pop("city")
print(removed)     # Delhi
print(student)     # {'name': 'Arjun', 'age': 21, 'gpa': 3.7}

# With a default — avoids KeyError if key is missing
val = student.pop("email", None)
print(val)         # None — no error

popitem() — Remove and Return the Last Inserted Item

student = {"name": "Arjun", "age": 21, "gpa": 3.7}

key, value = student.popitem()
print(key, value)    # gpa 3.7 — last inserted item
print(student)       # {'name': 'Arjun', 'age': 21}

popitem() is useful when processing a dictionary as a stack — removing and processing items one at a time.

del — Remove by Key

student = {"name": "Arjun", "age": 21, "gpa": 3.7}

del student["gpa"]
print(student)    # {'name': 'Arjun', 'age': 21}

# del student["email"]    # KeyError if key does not exist

clear() — Empty the Entire Dictionary

student = {"name": "Arjun", "age": 21}
student.clear()
print(student)    # {}  — empty dict, variable still exists

Iterating Over Dictionaries

This is where dictionaries become genuinely powerful. Three views let you iterate in different ways.

Iterating Over Keys

student = {"name": "Priya", "age": 20, "city": "Bangalore", "gpa": 3.9}

# Default iteration gives keys
for key in student:
    print(key)

# Explicit — same result
for key in student.keys():
    print(key)

Iterating Over Values

for value in student.values():
    print(value)

Iterating Over Key-Value Pairs With items()

This is the most common and most useful iteration pattern:

for key, value in student.items():
    print(f"{key}: {value}")

Output:

name: Priya
age: 20
city: Bangalore
gpa: 3.9

items() is the pattern you will use in the vast majority of real code when you need both the key and the value together.

# Building a formatted profile from a dictionary
profile = {
    "Name": "Priya Sharma",
    "Age": 20,
    "City": "Bangalore",
    "GPA": 3.9,
    "Enrolled": True
}

print("=== Student Profile ===")
for field, data in profile.items():
    print(f"{field:<12}: {data}")

Output:

=== Student Profile ===
Name        : Priya Sharma
Age         : 20
City        : Bangalore
GPA         : 3.9
Enrolled    : True

Copying Dictionaries — The Same Trap as Lists

Just like lists in Day 5, assigning a dictionary to a new variable does not copy it — both names reference the same object:

original = {"name": "Alice", "score": 88}
alias = original    # NOT a copy

alias["score"] = 100
print(original)    # {'name': 'Alice', 'score': 100} — changed!

Create a true independent copy with .copy():

original = {"name": "Alice", "score": 88}
copy = original.copy()

copy["score"] = 100
print(original)    # {'name': 'Alice', 'score': 88} — unchanged
print(copy)        # {'name': 'Alice', 'score': 100}

.copy() creates a shallow copy — sufficient when values are simple types. For dictionaries containing other mutable objects like lists or nested dicts, use copy.deepcopy():

import copy

original = {"name": "Alice", "scores": [88, 92, 79]}
deep = copy.deepcopy(original)

deep["scores"].append(95)
print(original["scores"])    # [88, 92, 79] — unchanged
print(deep["scores"])        # [88, 92, 79, 95]

Nested Dictionaries — Dictionaries Inside Dictionaries

Real data is rarely flat. A user has a profile, address, and list of orders. A product has pricing, inventory, and categories. Nested dictionaries represent this structure naturally:

company = {
    "name": "Schoolabe",
    "founded": 2024,
    "team": {
        "ceo": {"name": "Rohit", "age": 32},
        "cto": {"name": "Ananya", "age": 29},
    },
    "products": ["Python Course", "Kafka Course", "DSA Course"]
}

# Accessing nested values
print(company["name"])                    # Schoolabe
print(company["team"]["ceo"]["name"])     # Rohit
print(company["products"][0])             # Python Course

# Adding to a nested structure
company["team"]["designer"] = {"name": "Kabir", "age": 26}
print(company["team"]["designer"])        # {'name': 'Kabir', 'age': 26}

Iterating Over Nested Dictionaries

students = {
    "S001": {"name": "Alice", "grade": "A", "score": 92},
    "S002": {"name": "Bob",   "grade": "B", "score": 83},
    "S003": {"name": "Carol", "grade": "A", "score": 95},
}

for student_id, details in students.items():
    print(f"{student_id}: {details['name']} — {details['grade']} ({details['score']})")

Output:

S001: Alice — A (92)
S002: Bob — B (83)
S003: Carol — A (95)

Dictionary Comprehensions

Dictionary comprehensions create a new dictionary by applying an expression to each item in an iterable — the same idea as list comprehensions but producing key-value pairs:

# Basic syntax
new_dict = {key_expr: value_expr for item in iterable}
# Square numbers as a dictionary
squares = {n: n ** 2 for n in range(1, 6)}
print(squares)    # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# Word lengths
words = ["python", "java", "javascript", "go", "rust"]
lengths = {word: len(word) for word in words}
print(lengths)

# Invert a dictionary — swap keys and values
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
print(inverted)    # {1: 'a', 2: 'b', 3: 'c'}

With a Condition

scores = {"Alice": 88, "Bob": 55, "Carol": 92, "David": 61, "Eve": 78}

# Only students who passed (60+)
passed = {name: score for name, score in scores.items() if score >= 60}
print(passed)

# Grade assignment
def grade(score):
    return "A" if score >= 85 else "B" if score >= 70 else "C" if score >= 60 else "F"

graded = {name: grade(score) for name, score in scores.items()}
print(graded)

Merging Dictionaries

Three ways to merge dictionaries, each with different behavior:

update() — Merge Into Existing Dictionary

defaults = {"theme": "light", "language": "en", "notifications": True}
user_prefs = {"theme": "dark", "font_size": 14}

defaults.update(user_prefs)    # user_prefs values overwrite defaults
print(defaults)

| Operator — Create New Merged Dictionary (Python 3.9+)

defaults = {"theme": "light", "language": "en"}
user_prefs = {"theme": "dark", "font_size": 14}

merged = defaults | user_prefs    # right side wins on conflicts
print(merged)

# Original dicts unchanged
print(defaults)

** Unpacking — Works in All Python 3 Versions

merged = {**defaults, **user_prefs}    # right side wins on conflicts
print(merged)

The ** unpacking approach is the most widely compatible — use it when you need to support Python versions before 3.9.

Handling Missing Keys — Three Patterns

Missing keys are one of the most common sources of bugs in dictionary-heavy code. Three patterns handle them cleanly.

Pattern 1: Check First With in

config = {"host": "localhost", "port": 5432}

if "password" in config:
    password = config["password"]
else:
    password = "default_password"

Pattern 2: Use get() With a Default

password = config.get("password", "default_password")

Cleaner and more Pythonic than the if/else version.

Pattern 3: collections.defaultdict

When you need a dictionary where every missing key automatically gets a default value:

from collections import defaultdict

# Default value is an empty list for every missing key
word_positions = defaultdict(list)

sentence = "the quick brown fox jumps over the lazy fox"
for i, word in enumerate(sentence.split()):
    word_positions[word].append(i)

print(dict(word_positions))

Without defaultdict you would need setdefault() or an if key not in dict check before every append(). defaultdict eliminates that boilerplate entirely.

A Complete Working Program

Here is a student grade management system that uses every dictionary concept from this chapter together:

# Student grade management system
students = {
    "S001": {"name": "Priya Sharma",  "scores": [88, 92, 79, 95], "enrolled": True},
    "S002": {"name": "Arjun Mehta",   "scores": [72, 68, 81, 75], "enrolled": True},
    "S003": {"name": "Cleo Park",     "scores": [95, 98, 92, 97], "enrolled": True},
    "S004": {"name": "Ravi Kumar",    "scores": [55, 60, 58, 62], "enrolled": False},
    "S005": {"name": "Maya Torres",   "scores": [83, 87, 90, 85], "enrolled": True},
}

def calculate_average(scores):
    return sum(scores) / len(scores)

def assign_grade(average):
    if average >= 90: return "A"
    elif average >= 80: return "B"
    elif average >= 70: return "C"
    elif average >= 60: return "D"
    else: return "F"

# Build report using dict comprehension
report = {
    sid: {
        "name": data["name"],
        "average": round(calculate_average(data["scores"]), 1),
        "grade": assign_grade(calculate_average(data["scores"])),
        "enrolled": data["enrolled"]
    }
    for sid, data in students.items()
}

# Print full report
print(f"{'ID':<6} {'Name':<18} {'Average':>8} {'Grade':>6} {'Status':>10}")
print("-" * 52)
for sid, result in report.items():
    status = "Active" if result["enrolled"] else "Inactive"
    print(f"{sid:<6} {result['name']:<18} {result['average']:>8} "
          f"{result['grade']:>6} {status:>10}")

# Summary statistics
active = {sid: r for sid, r in report.items() if r["enrolled"]}
averages = [r["average"] for r in active.values()]

print(f"\n=== Summary ===")
print(f"Total students:  {len(report)}")
print(f"Active students: {len(active)}")
print(f"Class average:   {sum(averages)/len(averages):.1f}")
print(f"Top student:     {max(active.items(), key=lambda x: x[1]['average'])[1]['name']}")

Output:

ID     Name               Average  Grade     Status
----------------------------------------------------
S001   Priya Sharma           88.5      B     Active
S002   Arjun Mehta            74.0      C     Active
S003   Cleo Park              95.5      A     Active
S004   Ravi Kumar             58.8      F   Inactive
S005   Maya Torres            86.2      B     Active

=== Summary ===
Total students:  5
Active students: 4
Class average:   86.0
Top student:     Cleo Park

5 Dictionary Mistakes Every Beginner Makes

Mistake 1: Using bracket notation for a key that might not exist

config = {"host": "localhost"}
# print(config["port"])    # KeyError: 'port'

# Fix
print(config.get("port", 5432))    # 5432

Mistake 2: Duplicate keys silently overwriting each other

data = {"name": "Alice", "age": 25, "name": "Bob"}
print(data)    # {'name': 'Bob', 'age': 25} — Alice silently lost

Python does not warn you about duplicate keys. The last value wins. Always check for accidental duplication when building dictionaries from data sources.

Mistake 3: Iterating and modifying a dictionary simultaneously

scores = {"Alice": 88, "Bob": 55, "Carol": 92}

# Wrong — RuntimeError: dictionary changed size during iteration
# for name in scores:
#     if scores[name] < 60:
#         del scores[name]

# Fix — iterate over a copy of keys
for name in list(scores.keys()):
    if scores[name] < 60:
        del scores[name]

# Cleaner fix — dict comprehension
scores = {name: score for name, score in scores.items() if score >= 60}

Mistake 4: Forgetting that assignment aliases, not copies

original = {"name": "Alice", "score": 88}
copy = original           # same object in memory

copy["score"] = 100
print(original["score"])  # 100 — original changed unexpectedly

# Fix
copy = original.copy()

Mistake 5: Assuming keys(), values(), and items() return lists

student = {"name": "Alice", "age": 20}

keys = student.keys()
print(type(keys))    # <class 'dict_keys'> — not a list

# These are dynamic views — they update when the dict changes
student["city"] = "Delhi"
print(keys)    # dict_keys(['name', 'age', 'city']) — updated automatically

# Convert to list when you need list operations
keys_list = list(student.keys())

Practice: Dictionary Exercises

Exercise 1: Build and access

Create a dictionary representing a book with keys for title, author, year, genre, and rating. Access each value using both bracket notation and get(). Try accessing a key that does not exist using get() with a default.

Exercise 2: Update and remove

Start with a product dictionary containing name, price, and stock. Add a category key, update the price, remove the stock key using pop(), and print the result at each step.

Exercise 3: Iterate with items()

Given a dictionary of country-capital pairs for five countries, iterate over it using items() and print each pair in the format "The capital of [country] is [capital]".

Exercise 4: Dictionary comprehension

Given temperatures = {"Delhi": 38, "Mumbai": 32, "Bangalore": 26, "Chennai": 35, "Kolkata": 34}, create a new dictionary containing only cities where the temperature exceeds 33 degrees.

Exercise 5: Nested dictionary

Build a nested dictionary for three students, each with a name, a list of three scores, and an enrolled status. Write a loop that prints each student's name and their average score.

Exercise 6: Word frequency counter

Write a program that takes a sentence, splits it into words, and builds a dictionary counting how many times each word appears. Print the words sorted by frequency in descending order.

See all Python practice exercises with solutions

For all topics, see Python exercises.

What Comes Next — Day 8: Conditional Statements

You now have the complete beginner toolkit for Python's core data structures — lists, tuples, sets, and dictionaries. Day 8 puts them to work: conditional statements let your programs make decisions. Every if statement evaluates a condition and chooses a path — and now that you have rich data structures to query, your conditions can be meaningful and complex.

Day 8 covers:

  • if, elif, and else — the full decision structure
  • Nested conditions and compound conditions
  • Ternary expressions — one-line conditionals
  • Common patterns: range checks, membership tests, type checks
  • Writing conditions that are readable, not just correct

Continue to Day 8: Conditional Statements

Chapter navigation

Frequently asked questions: Dictionaries

What is a dictionary in Python?

A dictionary is an ordered, mutable collection that stores data as key-value pairs. Each value is associated with a unique key — you use the key to retrieve its value, similar to looking up a word in a physical dictionary. Dictionaries are one of the most used data structures in Python because most real-world data — JSON responses, configuration files, database records — naturally fits the key-value model.

What is the difference between dict[key] and dict.get(key) in Python?

Bracket notation raises a KeyError if the key does not exist. get() returns None by default, or a custom default you specify, without raising an error. Use brackets when the key must exist and its absence is a bug. Use get() when the key might legitimately be missing and you have a sensible default value.

Can Python dictionary keys be any data type?

Keys must be hashable — immutable types work: strings, integers, floats, tuples of hashable items. Mutable types cannot be keys: lists, sets, and other dicts are not hashable. In practice, string keys are by far the most common because they produce the most readable code.

How do I iterate over a Python dictionary?

Three methods cover all cases. for key in dict iterates over keys. for value in dict.values() iterates over values. for key, value in dict.items() iterates over key-value pairs together — this is the most common pattern because you usually need both the key and value in the loop body.

What is a nested dictionary in Python?

A nested dictionary is a dictionary where one or more values are themselves dictionaries. This structure represents hierarchical data naturally — a user with a profile and an address, a company with departments and employees. Access nested values by chaining bracket notation: data["user"]["address"]["city"].

What is a dictionary comprehension in Python?

A dictionary comprehension creates a new dictionary using a compact expression: {key_expr: value_expr for item in iterable}. It is the dict equivalent of a list comprehension — cleaner and often faster than building a dictionary with a loop and repeated assignment. Add a condition with if to filter which items are included.

How do I safely handle missing keys in a Python dictionary?

Three approaches: use in to check before accessing, use get() with a default value, or use collections.defaultdict when every missing key should automatically receive the same default. get() is the most concise for single lookups. defaultdict is best when you are accumulating data into lists or sets under dictionary keys.

What is the difference between pop() and del for removing dictionary items?

del dict[key] removes the key-value pair but returns nothing. dict.pop(key) removes the pair and returns the value, which you can use immediately. Both raise KeyError if the key is missing — unless you provide a default to pop(): dict.pop(key, None) returns None instead of raising an error.