Skip to main content

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:

  1. We create a student record with mixed types (strings and integers)
  2. We access values safely, knowing which keys exist
  3. We use .get() with a default for optional fields
  4. 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()

ScenarioUse ThisWhy
Key definitely existsdict[key]Direct access, fails loudly if wrong
Key might not exist.get(key, default)Safe fallback, no error
You want an error if missingdict[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): 1999
  • pages (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 value
  • rating (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): 8080
  • ssl_enabled (boolean): True
  • cache_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: 28
  • verified: 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:

  1. student['gpa']
  2. 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:

  1. Creates the user dictionary with sample data
  2. Accesses name and email with bracket notation
  3. 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:

  1. Will it run without errors?
  2. What are the type hints saying?
  3. 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.