My Notes
Python Dictionaries - The Complete Breakdown
Python Dictionaries — The Complete Breakdown
So what even is a dictionary?
A list stores things in order and you access them by position — skills[0], skills[1]. That works fine until you're storing something that has named properties. A user has a name, an age, an email, a city. Accessing that as user[0], user[1], user[2] is a nightmare — you have to remember what position maps to what.
A dictionary solves this. Instead of positions, you use names — called keys. Every key maps to a value. You look something up by name, not by number.
user = {
"name": "Kalyan",
"age": 22,
"city": "Boston"
}
print(user["name"]) # Kalyan
print(user["age"]) # 22
That's it. "name" is the key, "Kalyan" is the value. You give it a key, it gives you back the value.
Creating a dictionary
Three ways to create one:
# literal syntax — most common
user = {"name": "Kalyan", "age": 22}
# empty first, add later
config = {}
config["debug"] = True
config["port"] = 8080
# dict() constructor
settings = dict(theme="dark", language="python")
print(user)
print(config)
print(settings)
Output:
{'name': 'Kalyan', 'age': 22}
{'debug': True, 'port': 8080}
{'theme': 'dark', 'language': 'python'}
All three produce dictionaries. The literal {} syntax is what you'll use 95% of the time.
Accessing values
By key — dict[key]
user = {"name": "Kalyan", "age": 22, "city": "Boston"}
print(user["name"])
print(user["city"])
Output:
Kalyan
Boston
One rule: keys are case-sensitive. user["Name"] and user["name"] are completely different keys. user["Name"] would throw a KeyError on the dictionary above.
The safe way — .get()
Accessing a key that doesn't exist crashes with a KeyError:
print(user["email"]) # KeyError: 'email'
.get() returns None instead of crashing:
print(user.get("email")) # None
print(user.get("email", "N/A")) # N/A — custom default
Output:
None
N/A
Use .get() whenever the key might not exist — which is most of the time in real code. The second argument is your fallback default.
Adding and updating
Same syntax for both — if the key exists, it updates. If it doesn't, it adds.
user = {"name": "Kalyan", "age": 22}
user["city"] = "Boston" # add new key
user["age"] = 23 # update existing key
print(user)
Output:
{'name': 'Kalyan', 'age': 23, 'city': 'Boston'}
Adding multiple keys at once — .update()
user = {"name": "Kalyan"}
user.update({
"age": 22,
"city": "Boston",
"role": "student"
})
print(user)
Output:
{'name': 'Kalyan', 'age': 22, 'city': 'Boston', 'role': 'student'}
.update() merges another dictionary in. Existing keys get overwritten, new keys get added.
Deleting keys
Three ways:
user = {"name": "Kalyan", "age": 22, "city": "Boston", "temp": "delete me"}
del user["temp"] # delete by key — crashes if key missing
removed = user.pop("city") # delete and return the value
user.clear() # wipe everything
print(removed) # Boston
print(user) # {}
Output:
Boston
{}
Use del when you just want it gone. Use .pop() when you need the value before removing it. Use .clear() when you want to empty the whole dictionary.
.pop() also accepts a default to avoid crashing if the key doesn't exist:
val = user.pop("missing_key", "not found")
print(val) # not found
Checking if a key exists — in
user = {"name": "Kalyan", "age": 22}
print("name" in user) # True
print("email" in user) # False
print("age" not in user) # False
Always check with in before accessing a key you're not sure about — or just use .get().
All the dictionary methods
user = {"name": "Kalyan", "age": 22, "city": "Boston"}
print(user.keys()) # all keys
print(user.values()) # all values
print(user.items()) # all key-value pairs as tuples
print(len(user)) # number of keys
Output:
dict_keys(['name', 'age', 'city'])
dict_values(['Kalyan', 22, 'Boston'])
dict_items([('name', 'Kalyan'), ('age', 22), ('city', 'Boston')])
3
These return special view objects — not lists. But you can convert them:
keys = list(user.keys())
values = list(user.values())
Looping over a dictionary
Loop over keys (default)
user = {"name": "Kalyan", "age": 22, "city": "Boston"}
for key in user:
print(key)
Output:
name
age
city
Loop over values
for value in user.values():
print(value)
Output:
Kalyan
22
Boston
Loop over both — .items()
This is the one you'll use most:
for key, value in user.items():
print(f"{key}: {value}")
Output:
name: Kalyan
age: 22
city: Boston
items() gives you each key-value pair as a tuple, and you unpack it into key, value just like enumerate() did with lists.
Nested dictionaries — dicts inside dicts
A value in a dictionary can be anything — including another dictionary.
users = {
"kalyan": {
"age": 22,
"city": "Boston",
"skills": ["python", "security"]
},
"jordan": {
"age": 25,
"city": "New York",
"skills": ["javascript", "react"]
}
}
print(users["kalyan"]["city"])
print(users["jordan"]["skills"][0])
Output:
Boston
javascript
Chain [] accesses to go deeper. This is exactly how JSON data looks — and JSON is everywhere in APIs, config files, and web development.
Dictionary comprehensions — building dicts in one line
Same idea as list comprehensions, but for dictionaries.
scores = {"Alice": 85, "Bob": 92, "Charlie": 71, "Dave": 88}
passing = {name: score for name, score in scores.items() if score >= 80}
print(passing)
Output:
{'Alice': 85, 'Bob': 92, 'Dave': 88}
Pattern: {key_expr: value_expr for key, value in dict.items() if condition}.
Another example — square every number:
numbers = {"a": 2, "b": 3, "c": 4}
squared = {k: v ** 2 for k, v in numbers.items()}
print(squared)
Output:
{'a': 4, 'b': 9, 'c': 16}
Merging dictionaries
Python 3.9+ — the | operator
defaults = {"theme": "dark", "language": "en", "debug": False}
overrides = {"theme": "light", "debug": True}
config = defaults | overrides
print(config)
Output:
{'theme': 'light', 'language': 'en', 'debug': True}
Right side wins on conflicts. Clean and readable.
Python 3.8 and below — .update() or **unpacking
config = {**defaults, **overrides}
Same result. The ** unpacks both dicts into a new one. Right side wins on conflicts.
setdefault() — add a key only if it doesn't exist
user = {"name": "Kalyan"}
user.setdefault("age", 22) # key doesn't exist → adds it
user.setdefault("name", "Bob") # key exists → does nothing
print(user)
Output:
{'name': 'Kalyan', 'age': 22}
Useful when building up a dictionary incrementally and you don't want to overwrite existing values.
defaultdict — automatic default values
From the collections module. If you access a key that doesn't exist, instead of crashing it creates it with a default value automatically.
from collections import defaultdict
scores = defaultdict(list)
scores["Alice"].append(85)
scores["Alice"].append(92)
scores["Bob"].append(78)
print(dict(scores))
Output:
{'Alice': [85, 92], 'Bob': [78]}
Without defaultdict, scores["Alice"].append(85) would crash because "Alice" doesn't exist yet and you can't append to nothing. With defaultdict(list), accessing any missing key automatically creates an empty list there first.
Counter — counting made easy
Also from collections. Counts occurrences of items automatically.
from collections import Counter
words = ["python", "security", "python", "ctf", "python", "security"]
counts = Counter(words)
print(counts)
print(counts["python"])
print(counts.most_common(2))
Output:
Counter({'python': 3, 'security': 2, 'ctf': 1})
3
[('python', 3), ('security', 2)]
Counter is a dictionary under the hood. .most_common(n) returns the top n most frequent items.
OrderedDict — when insertion order matters
In Python 3.7+, regular dictionaries maintain insertion order anyway. But OrderedDict from collections is still useful when you need to explicitly move items or compare order:
from collections import OrderedDict
od = OrderedDict()
od["first"] = 1
od["second"] = 2
od["third"] = 3
od.move_to_end("first") # move to end
print(list(od.keys()))
Output:
['second', 'third', 'first']
In practice you'll rarely need this — regular dicts keep order fine. But it exists.
Dict vs list — when to use which
| Situation | Use |
|---|---|
| Ordered collection of items | list |
| Named properties of one thing | dict |
| Fast lookup by name | dict |
| Counting occurrences | Counter (dict) |
| Grouping items under keys | defaultdict (dict) |
| Representing a real-world object | dict |
| Representing JSON data | dict |
The mental model: if you'd naturally describe it with labels ("the user's name is...", "the config setting for..."), it's a dictionary. If you'd naturally describe it as items in sequence ("the first score", "the next item"), it's a list.
The bridge to OOP — why this matters
Here's the thing. A class in Python is basically a dictionary with functions attached. Look at this:
# dictionary version
user = {
"name": "Kalyan",
"age": 22
}
print(user["name"])
# class version (coming next)
class User:
def __init__(self, name, age):
self.name = name
self.age = age
u = User("Kalyan", 22)
print(u.name)
self.name in a class is doing almost exactly the same thing as user["name"] in a dictionary. The class just adds structure, methods, and dot-notation access on top.
This is why dictionaries come before OOP. Once you understand key: value, self.attribute = value makes immediate sense. It's the same idea, different syntax.
Real project — a contact book
A complete mini-project using everything in this file.
from collections import defaultdict
contacts = {}
def add_contact(name, phone, email, tags=None):
"""Add a new contact."""
if name in contacts:
print(f"Contact '{name}' already exists. Use update_contact() instead.")
return
contacts[name] = {
"phone": phone,
"email": email,
"tags": tags or []
}
print(f"Added: {name}")
def update_contact(name, **kwargs):
"""Update fields on an existing contact."""
if name not in contacts:
print(f"Contact '{name}' not found.")
return
contacts[name].update(kwargs)
print(f"Updated: {name}")
def get_contact(name):
"""Look up a contact by name."""
contact = contacts.get(name)
if not contact:
print(f"No contact found for '{name}'.")
return
print(f"\n{name}")
for field, value in contact.items():
print(f" {field}: {value}")
def search_by_tag(tag):
"""Find all contacts with a given tag."""
results = [name for name, data in contacts.items() if tag in data["tags"]]
if results:
print(f"\nContacts tagged '{tag}': {', '.join(results)}")
else:
print(f"No contacts found with tag '{tag}'.")
def show_all():
"""Print a summary of all contacts."""
if not contacts:
print("No contacts saved.")
return
print(f"\nAll contacts ({len(contacts)} total):")
for name, data in contacts.items():
print(f" {name} — {data['phone']} — {data['email']}")
add_contact("Alice", "555-1001", "alice@mail.com", tags=["friend", "work"])
add_contact("Bob", "555-1002", "bob@mail.com", tags=["friend"])
add_contact("Kalyan", "555-1003", "k@neu.edu", tags=["work", "security"])
add_contact("Jordan", "555-1004", "jordan@mail.com", tags=["work"])
update_contact("Bob", email="bob.new@mail.com", phone="555-9999")
get_contact("Kalyan")
search_by_tag("work")
show_all()
Output:
Added: Alice
Added: Bob
Added: Kalyan
Added: Jordan
Updated: Bob
Kalyan
phone: 555-1003
email: k@neu.edu
tags: ['work', 'security']
Contacts tagged 'work': Alice, Kalyan, Jordan
All contacts (4 total):
Alice — 555-1001 — alice@mail.com
Bob — 555-9999 — bob.new@mail.com
Kalyan — 555-1003 — k@neu.edu
Jordan — 555-1004 — jordan@mail.com
Every concept in this file is in that project. Nested dicts, .get(), .update(), .items(), in checks, dict comprehensions, f-strings, functions, exception-safe defaults — all of it wired together into something real.
The one-line summary
A dictionary maps keys to values. You access, add, update, and delete by key name — not position. Loop with .items() to get both. Use .get() to avoid crashes. Nest them for complex data. Master dictionaries and you instantly understand JSON, APIs, config files, and the foundation of how classes work.