Python: Zero to Hero
Home/Python Essentials
Share

Chapter 12: Dictionaries — Key-Value Data

Lists store items by position. You get item number 0, item number 1, item number 2. That works when position is meaningful. But often you want to look something up by name, not by index.

Imagine a contact list. With a list, you'd need to remember that position 0 is the name, position 1 is the phone number, position 2 is the email. Fragile and unclear. With a dictionary, you look up contact["phone"] and get the phone number directly. No guessing, no counting.

Dictionaries are Python's most versatile data structure. You'll use them constantly — for configuration, for counting, for grouping data, for caching, for representing real-world objects. Master this chapter and a huge portion of real Python code becomes easy to read and write.

What is a Dictionary?

A dictionary stores key-value pairs. Every value has a unique key that you use to look it up. Think of a real dictionary: you look up a word (the key) and find its definition (the value).

person = {
    "name": "Alice",
    "age": 30,
    "city": "London"
}

print(person)

Output:

{'name': 'Alice', 'age': 30, 'city': 'London'}

The syntax: curly braces {}, with key: value pairs separated by commas. Keys are usually strings (though any immutable type works). Values can be anything — strings, numbers, lists, other dictionaries, functions.

In plain English: A dictionary is like a labeled filing cabinet. Each drawer has a label (the key). You open the drawer to find what's inside (the value). You don't need to count to drawer 3 — you just ask for the drawer labeled "phone".

Creating a Dictionary

# Empty dictionary
empty = {}

# String keys
config = {
    "host": "localhost",
    "port": 5432,
    "database": "myapp",
    "debug": True
}

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

# Mixed value types
student = {
    "name": "Carlos",
    "grade": 88,
    "subjects": ["Math", "Physics", "Chemistry"],
    "active": True
}

You can also create a dictionary from a list of key-value pairs using dict():

pairs = [("name", "Alice"), ("age", 30), ("city", "London")]
person = dict(pairs)
print(person)   # {'name': 'Alice', 'age': 30, 'city': 'London'}

Or with keyword arguments:

person = dict(name="Alice", age=30, city="London")
print(person)   # {'name': 'Alice', 'age': 30, 'city': 'London'}

Accessing Values

Use the key in square brackets:

person = {"name": "Alice", "age": 30, "city": "London"}

print(person["name"])   # Alice
print(person["age"])    # 30

What if the key doesn't exist?

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

A KeyError crashes your program. Use .get() to avoid this.

.get() — Safe access with a default

print(person.get("email"))             # None  (no crash)
print(person.get("email", "N/A"))      # N/A   (your default)
print(person.get("name", "Unknown"))   # Alice (key exists, returns it)

.get() is one of the most used dictionary methods. Use it whenever the key might not be there.

Your turn: Create a dictionary for a movie with keys title, director, year, and rating. Print each value using both square bracket access and .get(). Try .get() with a key that doesn't exist and a default value.

Adding and Updating Values

Assign to a key to add or update:

person = {"name": "Alice", "age": 30}

# Update existing key
person["age"] = 31
print(person)   # {'name': 'Alice', 'age': 31}

# Add new key
person["email"] = "alice@example.com"
print(person)   # {'name': 'Alice', 'age': 31, 'email': 'alice@example.com'}

If the key exists, the value is overwritten. If it doesn't, a new key-value pair is added.

update() — Add or overwrite multiple pairs at once

person = {"name": "Alice", "age": 30}

person.update({"age": 31, "city": "Paris", "job": "Engineer"})
print(person)
# {'name': 'Alice', 'age': 31, 'city': 'Paris', 'job': 'Engineer'}

update() is useful when you have a batch of changes.

Deleting Keys

person = {"name": "Alice", "age": 30, "city": "London", "temp": "delete me"}

# del — raises KeyError if key doesn't exist
del person["temp"]

# pop() — removes and returns the value; optional default avoids KeyError
city = person.pop("city")
print(city)     # London
print(person)   # {'name': 'Alice', 'age': 30}

# pop with default — no error if key missing
result = person.pop("missing_key", "not found")
print(result)   # not found

# clear() — remove everything
person.clear()
print(person)   # {}

Checking if a Key Exists

person = {"name": "Alice", "age": 30}

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

Always check before accessing if you're not sure the key is there — or use .get().

# Safe pattern
if "email" in person:
    print(person["email"])
else:
    print("No email on file.")

Looping Over a Dictionary

Python gives you three ways to loop over a dictionary.

.keys() — Loop over keys

scores = {"Alice": 88, "Bob": 72, "Carlos": 95}

for name in scores.keys():
    print(name)

Output:

Alice
Bob
Carlos

In fact, looping over a dictionary directly also gives you the keys:

for name in scores:   # same as scores.keys()
    print(name)

.values() — Loop over values

for score in scores.values():
    print(score)

Output:

88
72
95

.items() — Loop over key-value pairs (most common)

for name, score in scores.items():
    print(f"{name}: {score}")

Output:

Alice: 88
Bob: 72
Carlos: 95

.items() returns each pair as a tuple, which you unpack directly in the for loop. This is the pattern you'll use most often — it gives you both the key and the value on every iteration.

Your turn: Create a dictionary of five countries and their populations. Loop over .items() and print each country and population in a formatted string. Also print the total world population of those five countries.

Dictionary Methods Reference

d = {"a": 1, "b": 2, "c": 3}

print(d.keys())    # dict_keys(['a', 'b', 'c'])
print(d.values())  # dict_values([1, 2, 3])
print(d.items())   # dict_items([('a', 1), ('b', 2), ('c', 3)])

print(len(d))      # 3

d2 = d.copy()      # shallow copy — independent from d

Nested Dictionaries

Dictionary values can be dictionaries themselves. This is how you represent structured, hierarchical data.

team = {
    "Alice": {"age": 30, "role": "Engineer", "salary": 95000},
    "Bob":   {"age": 25, "role": "Designer",  "salary": 75000},
    "Carlos":{"age": 35, "role": "Manager",   "salary": 110000},
}

# Access nested value
print(team["Alice"]["role"])     # Engineer
print(team["Carlos"]["salary"])  # 110000

# Loop over the nested structure
for name, info in team.items():
    print(f"{name}{info['role']} (${info['salary']:,})")

Output:

Alice — Engineer ($95,000)
Bob — Designer ($75,000)
Carlos — Manager ($110,000)

Nested dictionaries are everywhere in real data: API responses, configuration files, JSON data. Get comfortable with them.

Adding to a nested dict:

team["Diana"] = {"age": 28, "role": "QA Engineer", "salary": 80000}
team["Alice"]["salary"] = 100000   # raise Alice's salary

Your turn: Build a nested dictionary representing a small school. Top-level keys are class names ("Math", "Science"). Each value is a dictionary with "teacher" and "students" (a list of names). Print the teacher and number of students for each class.

Dictionary Comprehensions

Just like list comprehensions, you can create dictionaries in a single readable line.

The pattern: {key_expr: value_expr for item in iterable}

# Squares dictionary
squares = {n: n**2 for n in range(1, 6)}
print(squares)   # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# Invert a dictionary (swap keys and values)
original = {"a": 1, "b": 2, "c": 3}
inverted = {value: key for key, value in original.items()}
print(inverted)   # {1: 'a', 2: 'b', 3: 'c'}
# Filter: keep only passing scores
scores = {"Alice": 88, "Bob": 45, "Carlos": 92, "Diana": 38, "Eve": 76}
passing = {name: score for name, score in scores.items() if score >= 50}
print(passing)   # {'Alice': 88, 'Carlos': 92, 'Eve': 76}
# Convert a list of words to a dict of word -> length
words = ["apple", "banana", "cherry", "date"]
lengths = {word: len(word) for word in words}
print(lengths)   # {'apple': 5, 'banana': 6, 'cherry': 6, 'date': 4}

Your turn: Given a list of temperatures in Celsius [0, 10, 20, 30, 37, 100], create a dictionary mapping each Celsius value to its Fahrenheit equivalent using a dictionary comprehension.

The Counting Pattern

One of the most common uses of a dictionary is counting occurrences of items.

words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = {}

for word in words:
    if word in counts:
        counts[word] += 1
    else:
        counts[word] = 1

print(counts)   # {'apple': 3, 'banana': 2, 'cherry': 1}

A cleaner version using .get():

counts = {}
for word in words:
    counts[word] = counts.get(word, 0) + 1

print(counts)   # {'apple': 3, 'banana': 2, 'cherry': 1}

counts.get(word, 0) returns the current count (or 0 if not yet seen), then adds 1. This is the idiomatic Python counting pattern — you'll use it constantly.

defaultdict and Counter from collections

Python's standard library has two tools that make dictionary-based counting and grouping even easier.

Counter — Count things instantly

from collections import Counter

words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = Counter(words)

print(counts)
# Counter({'apple': 3, 'banana': 2, 'cherry': 1})

print(counts["apple"])    # 3
print(counts["grape"])    # 0  (not KeyError — returns 0 for missing keys)

# Most common
print(counts.most_common(2))
# [('apple', 3), ('banana', 2)]
# Count characters in a string
letter_counts = Counter("mississippi")
print(letter_counts)
# Counter({'s': 4, 'i': 4, 'p': 2, 'm': 1})

print(letter_counts.most_common(3))
# [('s', 4), ('i', 4), ('p', 2)]

Counter is a subclass of dict. It works anywhere a dict does, and it adds the .most_common() method.

defaultdict — Never get a KeyError again

defaultdict works like a regular dict, but if you access a missing key it automatically creates it with a default value.

from collections import defaultdict

# defaultdict with list — groups items
groups = defaultdict(list)

students = [("Math", "Alice"), ("Science", "Bob"),
            ("Math", "Carlos"), ("Science", "Diana"), ("Math", "Eve")]

for subject, name in students:
    groups[subject].append(name)

print(dict(groups))
# {'Math': ['Alice', 'Carlos', 'Eve'], 'Science': ['Bob', 'Diana']}

Without defaultdict, you'd need to check if subject in groups before appending. With it, the missing key is created with an empty list automatically.

# defaultdict with int — counting
from collections import defaultdict

word_count = defaultdict(int)

for word in ["apple", "banana", "apple", "cherry"]:
    word_count[word] += 1

print(dict(word_count))
# {'apple': 2, 'banana': 1, 'cherry': 1}

Your turn: Use Counter to count the frequency of each character in the string "programming". Print the three most common characters.

Merging Dictionaries

Python 3.9+: the | operator

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

settings = defaults | user_prefs
print(settings)
# {'theme': 'dark', 'font_size': 14, 'language': 'en'}

The right-hand dict's values win on conflicts. This is perfect for layering defaults with user overrides.

{**dict1, **dict2} — Works in all Python 3 versions

settings = {**defaults, **user_prefs}
print(settings)
# {'theme': 'dark', 'font_size': 14, 'language': 'en'}

** unpacks a dictionary into key-value pairs. Spreading two dicts inside {} merges them.

Putting It All Together: A Word Frequency Analyzer

from collections import Counter


def word_frequency(text, top_n=10):
    """
    Analyze word frequency in a text.
    Returns the top_n most common words and their counts.
    """
    # Clean the text and split into words
    stop_words = {"the", "a", "an", "is", "in", "it", "to", "and",
                  "of", "for", "on", "are", "was", "that", "this",
                  "with", "as", "at", "be", "by", "from", "or"}

    words = text.lower().split()
    cleaned = [
        word.strip(".,!?;:\"'()-")
        for word in words
        if word.strip(".,!?;:\"'()-") not in stop_words
        and len(word.strip(".,!?;:\"'()-")) > 1
    ]

    counts = Counter(cleaned)
    total  = sum(counts.values())

    print(f"Total meaningful words: {total}")
    print(f"Unique words:           {len(counts)}")
    print(f"\nTop {top_n} words:")
    print(f"{'Word':<15} {'Count':>5} {'Frequency':>10}")
    print("-" * 33)

    for word, count in counts.most_common(top_n):
        freq = count / total
        print(f"{word:<15} {count:>5} {freq:>10.1%}")

    return counts


sample = """
Python is an interpreted high-level programming language. Python's design
philosophy emphasizes code readability. Python is dynamically typed and
garbage-collected. It supports multiple programming paradigms, including
structured, object-oriented and functional programming. Python is often
described as a batteries-included language due to its comprehensive
standard library. Python is consistently ranked as one of the most popular
programming languages.
"""

frequency = word_frequency(sample, top_n=8)

Output:

Total meaningful words: 42
Unique words:           36

Top 8 words:
Word            Count  Frequency

python              5      11.9%
programming         3       7.1%
language            2       4.8%
design              1       2.4%
philosophy          1       2.4%
emphasizes          1       2.4%
code                1       2.4%
readability         1       2.4%

Counter, dictionary comprehension, f-string formatting, and loop unpacking — all working together.

What You Learned in This Chapter

  • A dictionary stores key-value pairs using {key: value} syntax.
  • Access values with dict[key] (raises KeyError if missing) or dict.get(key, default) (safe).
  • Add or update: dict[key] = value. Delete: del dict[key] or dict.pop(key).
  • Check membership: key in dict.
  • Loop with .keys(), .values(), or .items() (most common — gives both).
  • Nested dictionaries represent hierarchical data.
  • Dictionary comprehensions: {k: v for k, v in iterable if condition}.
  • The counting pattern: counts[word] = counts.get(word, 0) + 1.
  • Counter from collections — instant counting with .most_common().
  • defaultdict — auto-creates missing keys with a factory function.
  • Merge dicts with | (Python 3.9+) or {**d1, **d2}.

What's Next?

You now have all five core Python data structures: strings, lists, tuples, sets, and dictionaries. Together they can represent almost any data you'll encounter.

But programs fail. Files don't exist. Users type the wrong thing. Network connections drop. In Chapter 13 you'll learn error handling — how to use try, except, finally, and raise to write programs that deal gracefully with problems instead of crashing. It's the difference between code that works in a demo and code that works in production.

Your turn: Write a program that reads a text file (you create it with a few paragraphs of any text), counts the frequency of every word using a Counter, and prints: the 10 most common words, the 10 least common words, the total word count, and the unique word count. Handle the case where the file doesn't exist with a friendly error message.

© 2026 Abhilash Sahoo. Python: Zero to Hero.