Technology  /  Python

🐍 Python 78 guides · updated 2026

From first variable to OOP, generators, and real projects — the language that runs everything from data pipelines to AI agents, taught the practical way.

Python Dictionaries: Key-Value Pairs, Methods, and Real-World Patterns

Dictionaries are the backbone of Python data handling. JSON responses from APIs, configuration files, database rows, HTML attributes — almost any structured data maps naturally to a dictionary. Python’s dict is efficient, flexible, and has an excellent set of built-in methods.


Creating Dictionaries

# Literal syntax
person = {
"name": "Alice",
"age": 30,
"city": "Edinburgh"
}
# dict() constructor with keyword arguments
settings = dict(theme="dark", font_size=14, auto_save=True)
# From a list of (key, value) pairs
pairs = [("a", 1), ("b", 2), ("c", 3)]
lookup = dict(pairs)
# From two lists using zip
keys = ["x", "y", "z"]
values = [10, 20, 30]
coords = dict(zip(keys, values))
# Empty dict
empty = {}

Keys must be immutable (strings, numbers, tuples). Values can be anything.


Accessing Values

Square bracket notation

user = {"name": "Bob", "email": "bob@example.com", "active": True}
print(user["name"]) # "Bob"
print(user["email"]) # "bob@example.com"

If the key doesn’t exist, you get a KeyError:

print(user["phone"]) # KeyError: 'phone'

get() — the safe alternative

get() returns None (or a default you specify) when the key doesn’t exist:

print(user.get("name")) # "Bob"
print(user.get("phone")) # None
print(user.get("phone", "N/A")) # "N/A" — custom default

Use get() whenever you’re not certain a key exists. user["phone"] crashing is a runtime error; user.get("phone") returning None is a controllable situation.

setdefault()

Returns the value if the key exists; if not, inserts the key with a default value and returns it:

inventory = {"apples": 5}
inventory.setdefault("bananas", 0) # adds bananas: 0
inventory.setdefault("apples", 0) # no change — apples already exists
print(inventory) # {"apples": 5, "bananas": 0}

Useful for building up nested structures or counters.


Modifying Dictionaries

config = {"debug": False, "host": "localhost", "port": 8080}
# Update or add a key
config["debug"] = True
config["database"] = "postgres"
# Update multiple keys at once
config.update({"port": 9000, "timeout": 30})
# Remove a key
del config["timeout"]
removed = config.pop("database") # removes and returns the value
config.pop("missing", None) # safe pop — no error if key absent

Iterating Over Dictionaries

Since Python 3.7, dictionaries maintain insertion order:

scores = {"Alice": 92, "Bob": 85, "Charlie": 78}
# Keys only (default iteration)
for name in scores:
print(name)
# Values only
for score in scores.values():
print(score)
# Key-value pairs — the most common pattern
for name, score in scores.items():
print(f"{name}: {score}")

Merging Dictionaries

defaults = {"theme": "light", "language": "en", "notifications": True}
user_prefs = {"theme": "dark", "font_size": 14}
# Python 3.9+ — union operator
merged = defaults | user_prefs # user_prefs wins on conflicts
# Python 3.5+ — dict unpacking
merged = {**defaults, **user_prefs} # same behaviour
# In-place merge
defaults.update(user_prefs) # modifies defaults
print(merged)
# {'theme': 'dark', 'language': 'en', 'notifications': True, 'font_size': 14}

defaultdict from collections

When you need to handle missing keys with a factory function, defaultdict is cleaner than repeated setdefault() calls:

from collections import defaultdict
# Count word occurrences
text = "apple banana apple cherry banana apple"
word_count = defaultdict(int) # missing keys default to 0
for word in text.split():
word_count[word] += 1 # no KeyError on first access
print(dict(word_count)) # {'apple': 3, 'banana': 2, 'cherry': 1}
# Build lists of values per key
from collections import defaultdict
groups = defaultdict(list)
students = [("Math", "Alice"), ("Science", "Bob"), ("Math", "Charlie")]
for subject, student in students:
groups[subject].append(student)
print(dict(groups))
# {'Math': ['Alice', 'Charlie'], 'Science': ['Bob']}

Comprehensions

Dict comprehensions build dictionaries in a single expression:

names = ["Alice", "Bob", "Charlie"]
name_lengths = {name: len(name) for name in names}
# {'Alice': 5, 'Bob': 3, 'Charlie': 7}
# Filter while building
scores = {"Alice": 92, "Bob": 65, "Charlie": 78, "Diana": 88}
passing = {name: score for name, score in scores.items() if score >= 70}
# {'Alice': 92, 'Charlie': 78, 'Diana': 88}
# Invert a dictionary (swap keys and values)
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
# {1: 'a', 2: 'b', 3: 'c'}

Common Real-World Patterns

Counting occurrences

from collections import Counter
words = "the cat sat on the mat the cat".split()
counts = Counter(words)
print(counts.most_common(3)) # [('the', 3), ('cat', 2), ('sat', 1)]

Grouping records

orders = [
{"id": 1, "status": "shipped", "amount": 50},
{"id": 2, "status": "pending", "amount": 75},
{"id": 3, "status": "shipped", "amount": 120},
]
by_status = defaultdict(list)
for order in orders:
by_status[order["status"]].append(order)

Caching computed results

cache = {}
def expensive_calculation(n):
if n in cache:
return cache[n]
result = sum(range(n)) # simulate heavy computation
cache[n] = result
return result

Practical Tips

Use .get() instead of in + [] when you need the value anyway:

# Redundant
if "key" in d:
value = d["key"]
# Better
value = d.get("key")

Keys are case-sensitive. {"Name": "Alice"} and {"name": "Alice"} are different keys. If accepting user-supplied keys, normalise case consistently.

Avoid modifying a dictionary while iterating it. Create a list of changes to apply after iteration, or iterate over a copy.