Dictionaries Part 1: Key-Value Mappings
Imagine you're tracking information about students in a class. You could use a list:
students: list = ["Alice", 20, "Computer Science", 3.8]
But there's a problem: months later, do you remember which index holds the age? Is index 1 the age or the major? When you read that code again, it's confusing.
Dictionaries solve this problem by letting you use meaningful names (called keys) instead of mysterious numbers. Keys map to the values you care about. Instead of students[1], you write student["age"]. The code reads like English.
In this lesson, you'll learn how to create dictionaries, access their values safely, and use type hints to make your intent crystal clear to Python tools and other developers.
What Is a Dictionary?
A dictionary is a Python data structure that stores key-value pairs. Each key maps to one value, like a real dictionary where you look up a word (key) to find its definition (value).
Here's a simple example:
student: dict[str, str | int] = {
"name": "Alice",
"age": 20,
"major": "Computer Science"
}
In this dictionary:
- Keys are:
"name","age","major" - Values are:
"Alice",20,"Computer Science" - The
dict[str, str | int]type hint says: "Keys are strings, values can be either strings OR integers"
Key insight: Dictionaries are unordered mappings, not sequences like lists. You don't access values by position (like student[0])—you access them by meaningful keys (like student["age"]).
💬 AI Colearning Prompt
"Why would you use a dictionary instead of a list to store student information? What problem does a key-value structure solve?"
This question helps you think about the intent behind choosing a dictionary. You're not memorizing syntax—you're understanding when to use each structure.
Creating Dictionaries
There are two main ways to create dictionaries: literals (most common) and the dict() constructor (less common, useful for specific patterns).
Dictionary Literals
The most Pythonic way is to use curly braces {} with key: value pairs:
# Student record with multiple fields
student: dict[str, str | int] = {
"name": "Alice",
"age": 20,
"major": "Computer Science"
}
# Configuration settings (all string values)
config: dict[str, str] = {
"database_url": "postgres://localhost:5432",
"debug_mode": "true",
"api_key": "sk-..."
}
# Grade mapping (string keys, integer values)
grades: dict[str, int] = {
"Alice": 95,
"Bob": 87,
"Charlie": 92
}
# Empty dictionary (to fill later)
empty_dict: dict[str, int] = {}
Notice the type hints: Each one tells you what types keys and values expect.
🎓 Instructor Commentary
In AI-native development, type hints like
dict[str, int]aren't just syntax—they're communication. You're telling Python tools, your teammates, and AI collaborators: "Keys are strings, values are integers." This clarity unlocks better suggestions from AI and catches mismatches early.
Union Types for Mixed Values
Sometimes dictionary values have different types. Use the union operator | to express this:
# Student record with mixed types
student: dict[str, str | int] = {
"name": "Alice", # string value
"age": 20, # integer value
"major": "Computer Science" # string value
}
# Page configuration (strings and booleans mixed)
page_config: dict[str, str | bool] = {
"title": "Home", # string
"dark_mode": True, # boolean
"subtitle": "Welcome" # string
}
Why use union types? They document intent: "This dict can have multiple value types, and that's intentional."
🚀 CoLearning Challenge
Ask your AI Co-Teacher:
"Create a user profile dictionary with the following fields: username (string), age (integer), is_verified (boolean), email (string). Use proper type hints including union types. Then explain why we use dict[str, str | int | bool] instead of just dict."
Expected Outcome: You'll understand how union types document mixed-type data and why that clarity matters in real code.
The dict() Constructor
Less common, but useful when converting from other data:
# Create from list of tuples
pairs: list[tuple[str, int]] = [("Alice", 95), ("Bob", 87)]
grades: dict[str, int] = dict(pairs)
# Create empty dict
empty: dict[str, str] = dict()
For now, stick with literals ({}). The dict() constructor is useful in advanced scenarios.
Accessing Dictionary Values
Now you have a dictionary. How do you get values out?
Bracket Notation: Direct Access
The simplest way—use square brackets with the key:
student: dict[str, str | int] = {
"name": "Alice",
"age": 20,
"major": "Computer Science"
}
print(student["name"]) # Output: Alice
print(student["age"]) # Output: 20
print(student["major"]) # Output: Computer Science
This works perfectly when you know the key exists. But what if you try to access a key that doesn't exist?
KeyError: The Error You'll See
Access a non-existent key, and you get a KeyError:
student: dict[str, str | int] = {
"name": "Alice",
"age": 20,
"major": "Computer Science"
}
print(student["gpa"]) # KeyError: 'gpa'
The error message is clear: the key "gpa" doesn't exist. This is by design—Python tells you immediately when you ask for something that isn't there.
✨ Teaching Tip
Use Claude Code to experiment with KeyError. Ask: "What does the KeyError traceback tell me? Why is Python strict about missing keys?"
Understanding errors is part of learning. Python's strictness prevents silent bugs.
Safe Access with .get() Method
Often, you don't know if a key exists, and you want a default value if it's missing. Use the .get() method:
student: dict[str, str | int] = {
"name": "Alice",
"age": 20,
"major": "Computer Science"
}
# Safe access: returns "N/A" if "gpa" doesn't exist
gpa = student.get("gpa", "N/A")
print(gpa) # Output: N/A
# Safe access: returns "Unknown" if "hometown" doesn't exist
hometown = student.get("hometown", "Unknown")
print(hometown) # Output: Unknown
# Safe access: key exists, returns actual value
name = student.get("name", "Unknown")
print(name) # Output: Alice
Syntax: dict.get(key, default_value)
When to use .get():
- You're not sure if a key exists
- You want a sensible default if it's missing
- You want to avoid KeyError
When to use bracket notation (dict[key]):
- You're certain the key exists
- You want an error if the key is missing (catching bugs)
💬 AI Colearning Prompt
"I'm building a feature that retrieves user settings. Sometimes a setting doesn't exist. Should I use bracket notation or .get()? What are the tradeoffs?"
This question teaches you to think about edge cases—what happens when data is incomplete. The answer shapes how you design your code.
Important: Unique Keys
Every key in a dictionary must be unique. If you add a key that already exists, it overwrites the previous value:
config: dict[str, str] = {
"host": "localhost",
"port": "5432"
}
# Add a new setting
config["debug"] = "true"
print(config)
# Output: {'host': 'localhost', 'port': '5432', 'debug': 'true'}
# Overwrite an existing key
config["port"] = "3306" # Changed from 5432 to 3306
print(config)
# Output: {'host': 'localhost', 'port': '3306', 'debug': 'true'}
This is intentional. If you try to add a duplicate key, Python silently overwrites. This is often what you want (updating a setting), but it's worth knowing.
Practical Example: Student Record System
Let's build a real-world example—a system to track student records:
# Student record with type hints
student: dict[str, str | int] = {
"name": "Alice",
"age": 20,
"student_id": 12345,
"major": "Computer Science"
}
print(f"Name: {student['name']}")
print(f"ID: {student['student_id']}")
# Safely access a field that might not exist
gpa = student.get("gpa", "Not recorded")
print(f"GPA: {gpa}")
# Add a new field
student["gpa"] = 3.8
print(f"GPA (updated): {student['gpa']}")
# Safely update a field
student["age"] = student.get("age", 0) + 1
print(f"Age (updated): {student['age']}")
Output:
Name: Alice
ID: 12345
GPA: Not recorded
GPA (updated): 3.8
Age (updated): 21
What's happening:
- We create a student record with mixed types (strings and integers)
- We access values safely, knowing which keys exist
- We use
.get()with a default for optional fields - We update values by adding new keys or reassigning existing ones
This is how real programs track data—with dictionaries, not mysterious lists.
🎓 Instructor Commentary
Notice we used f-strings for output:
f"Name: {student['name']}". This is readable Python. The dictionary structure (meaningful keys) makes the code self-documenting. When you read this code in 6 months, you'll instantly understand what's stored and why.
Preview: Dictionary Methods
Dictionaries have several useful methods. You'll explore these deeply in Lesson 8, but here's a quick preview:
.keys()— List all keys:student.keys().values()— List all values:student.values().items()— List all key-value pairs:student.items()
student: dict[str, str | int] = {
"name": "Alice",
"age": 20,
"major": "Computer Science"
}
print(student.keys()) # dict_keys(['name', 'age', 'major'])
print(student.values()) # dict_values(['Alice', 20, 'Computer Science'])
print(student.items()) # dict_items([('name', 'Alice'), ('age', 20), ('major', 'Computer Science')])
These methods are useful when iterating (Lesson 9) or examining dictionary structure. For now, know they exist.
Quick Reference: Bracket vs .get()
| Scenario | Use This | Why |
|---|---|---|
| Key definitely exists | dict[key] | Direct access, fails loudly if wrong |
| Key might not exist | .get(key, default) | Safe fallback, no error |
| You want an error if missing | dict[key] | Catches bugs early |
| You want a sensible default | .get(key, default) | Handles missing data gracefully |
Practice Exercises
Exercise 1: Create a Typed Dictionary
Create a dictionary representing a book with the following fields:
title(string): "The Pragmatic Programmer"author(string): "David Thomas"year(integer): 1999pages(integer): 352
Write the dictionary with proper type hints. Then print the title and author.
# Write your solution here
book: dict[str, str | int] = {
# ... fill this in
}
print(f"Title: {book['title']}")
print(f"Author: {book['author']}")
Check your understanding: Can you explain what dict[str, str | int] means?
Exercise 2: Safe Access with .get()
Using the book dictionary from Exercise 1, safely access:
publisher(doesn't exist) with default "Unknown Publisher"pages(exists) to get the actual valuerating(doesn't exist) with default 0.0
Print all three values.
# Extend your solution from Exercise 1
publisher = book.get("publisher", "Unknown Publisher")
# ... continue with rating and pages
Check your understanding: Why does .get() not raise a KeyError?
Exercise 3: Union Types with Mixed Values
Create a dictionary representing a website configuration:
domain(string): "example.com"port(integer): 8080ssl_enabled(boolean): Truecache_ttl(integer): 3600
Use the correct type hint for mixed types. Then access and print each value.
# Write your solution here (remember: use dict[str, str | int | bool])
config: dict[str, str | int | bool] = {
# ... fill this in
}
# Print each value
Check your understanding: Why do we use str | int | bool instead of just str?
Exercise 4: Real-World Application
You're building a contact management system. Create a dictionary for a contact with:
name: "Alice Johnson"email: "[email protected]"phone: "555-1234"age: 28verified: True (boolean)
Access the contact's name and email. Then safely retrieve notes (which doesn't exist) with a default value of "No notes".
# Write your solution here (remember union types!)
contact: dict[str, str | int | bool] = {
# ... fill this in
}
# Access values and handle missing data
Check your understanding: What would happen if you tried contact["notes"] with bracket notation? How is .get() different?
Try With AI
Now it's time to validate your understanding with your AI companion. These prompts follow a progression from explanation to application.
Prompt 1: Explain Concepts (Understanding)
Ask your AI companion (Claude Code, Gemini CLI, or ChatGPT):
"Explain the difference between lists and dictionaries. When would you use a list to store student records? When would you use a dictionary? Give a concrete example for each."
Expected outcome: You'll articulate why dictionaries are better for named data. The AI response should emphasize meaningful keys versus numeric indices.
Prompt 2: Explore Trade-offs (Understanding + Application)
Ask your AI companion:
"I have two ways to access a dictionary value:
student['gpa']student.get('gpa', 'Not available')When should I use each? What happens if the key is missing in each case?"
Expected outcome: You'll understand the trade-off: bracket notation is strict (good for catching bugs), .get() is lenient (good for optional data). You'll choose the right method for your intent.
Prompt 3: Apply with Code (Application)
Ask your AI companion:
"I'm building a system that tracks user profiles. Create a typed dictionary for a user with name, email, age, and subscription_status (true/false). Write code that:
- Creates the user dictionary with sample data
- Accesses name and email with bracket notation
- Safely retrieves a 'premium_features' field that doesn't exist, with default 'Standard'
Then explain the type hints you used."
Expected outcome: You'll see a complete, practical example. You'll validate the code by running it. You'll reason about type hints with the AI.
Prompt 4: Validate and Extend (Application + Analysis)
Ask your AI companion:
"Review this code and tell me:
- Will it run without errors?
- What are the type hints saying?
- If I add
student['dob'] = '2001-05-15'(a new date-of-birth field), does the type hint still work? Why or why not?[Paste your solution from Exercise 3 or 4 here]"
Expected outcome: You'll learn to read type hints critically. You'll understand how type hints document intent but don't enforce at runtime. You'll think about what happens when data doesn't match the declared type.
Safety note: When experimenting with dictionaries, you might create nested structures (dicts inside dicts) by accident. That's okay—we'll cover nested dicts in Lesson 9. For now, focus on flat dictionaries with simple key-value pairs.