Python Lecture 7: Dictionaries and Sets - Advanced Data Structures for Real-World Programming
Welcome to an exciting lecture that will fundamentally change how you think about organizing data! In the previous lecture, you learned about lists and tuples - sequential collections where items are accessed by position. Today, we're exploring two incredibly powerful data structures: dictionaries (which store key-value pairs) and sets (which store unique elements).
These data structures are everywhere in real-world programming. When you log into a website, your credentials are looked up in a dictionary. When you search for a product, the database uses dictionary-like structures for lightning-fast lookups. When you check if an email address already exists in a system, sets ensure uniqueness. Understanding dictionaries and sets is crucial for building efficient, professional applications.
By the end of this comprehensive lecture, you'll understand how to store related data together using meaningful keys, perform ultra-fast lookups, ensure data uniqueness, and build sophisticated data structures that mirror real-world relationships. Let's begin!
Understanding Dictionaries - The Power of Key-Value Pairs
A dictionary is an unordered collection of key-value pairs. Think of it like a real dictionary where you look up a word (the key) to find its definition (the value). Or like a phone book where names (keys) map to phone numbers (values). This mapping relationship is one of the most useful concepts in programming.
Why Dictionaries Are Revolutionary: Lists force you to remember positions: "The student's name is at index 0, their age is at index 1, their grade is at index 2." This is fragile and unclear. Dictionaries let you use meaningful names: student["name"], student["age"], student["grade"]. Your code becomes self-documenting and much harder to break.
Real-World Analogy: Imagine organizing a school. A list would be: [student1, student2, student3...]. To find a specific student, you'd have to search through every position. A dictionary is like: {student_id: student_data}. You can instantly find any student by their ID without searching. This is the power of dictionaries - direct access by meaningful keys.
Speed Advantage: Here's something crucial: finding an item in a list requires checking each position until you find it (linear time). Finding an item in a dictionary by its key is nearly instant, regardless of dictionary size (constant time). For large datasets, this difference is massive - the difference between a program that takes hours versus seconds.
Creating Dictionaries - Multiple Approaches
Python provides several ways to create dictionaries, each useful in different scenarios. Understanding these patterns will make your code more flexible and Pythonic.
# Empty dictionary - starting point
empty_dict = {}
print(f"Empty: {empty_dict}")
# Dictionary with initial data
person = {
"name": "Alice",
"age": 25,
"city": "New York",
"employed": True
}
print(f"Person: {person}")
# Mixed value types (completely fine!)
mixed = {
"string": "hello",
"number": 42,
"list": [1, 2, 3],
"nested_dict": {"key": "value"}
}
print(f"Mixed: {mixed}")
# Using dict() constructor
student = dict(name="Bob", grade=85, major="CS")
print(f"Student: {student}")
# Creating from lists of pairs
pairs = [("a", 1), ("b", 2), ("c", 3)]
from_pairs = dict(pairs)
print(f"From pairs: {from_pairs}")
Key Restrictions - Important Rules: Dictionary keys must be immutable types: strings, numbers, or tuples. You cannot use lists, dictionaries, or sets as keys because they can change. This restriction exists for technical reasons related to how dictionaries achieve their speed. Values can be any type - no restrictions!
Real-World Application - User Profile System: When users register on a website, their information is stored in dictionary-like structures. Instead of remembering "email is position 2, password hash is position 3," you use clear keys: user["email"], user["password_hash"], user["registration_date"]. This makes code maintainable as your data structure grows from 5 fields to 50 fields.
Accessing Dictionary Values - Multiple Methods
Once you have data in a dictionary, you need to retrieve it. Python provides several ways to access dictionary values, each with different behavior when a key doesn't exist.
student = {
"name": "Alice",
"age": 20,
"major": "Computer Science",
"gpa": 3.8
}
# Direct access with square brackets
print(f"Name: {student['name']}")
print(f"GPA: {student['gpa']}")
# This would cause KeyError if key doesn't exist:
# print(student['phone']) # KeyError!
# Safe access with get() - returns None if key missing
phone = student.get('phone')
print(f"Phone: {phone}") # None
# get() with default value
phone = student.get('phone', 'Not provided')
print(f"Phone: {phone}") # Not provided
# Check if key exists before accessing
if 'email' in student:
print(f"Email: {student['email']}")
else:
print("Email not found")
Square Brackets vs get() - When to Use Each:
Use [] when: You know the key must exist. If it doesn't, you want the program to crash and alert you immediately. This is good for required fields where missing data is a bug.
Use get() when: The key might not exist and that's okay. You want to handle missing data gracefully. Perfect for optional fields like phone numbers, middle names, or preferences.
Professional Practice: In production code, always use get() for optional data and provide meaningful defaults. This prevents crashes and makes your program more robust. Reserve [] for cases where missing data truly indicates an error condition.
Modifying Dictionaries - Adding, Updating, and Removing Data
Dictionaries are mutable - you can change them after creation. You can add new key-value pairs, update existing values, or remove pairs entirely. This flexibility makes dictionaries perfect for data that evolves during program execution.
profile = {"name": "Bob", "age": 30}
print(f"Original: {profile}")
# Adding new key-value pairs
profile["email"] = "bob@email.com"
profile["city"] = "Boston"
print(f"After adding: {profile}")
# Updating existing values
profile["age"] = 31
print(f"After update: {profile}")
# update() method - add/update multiple items
profile.update({
"phone": "123-456-7890",
"occupation": "Engineer"
})
print(f"After bulk update: {profile}")
# Removing items
# pop() - removes and returns value
email = profile.pop("email")
print(f"Removed email: {email}")
print(f"After pop: {profile}")
# del - removes key-value pair
del profile["phone"]
print(f"After del: {profile}")
# clear() - removes all items
profile.clear()
print(f"After clear: {profile}")
Understanding update(): The update() method is incredibly useful when you need to merge dictionaries or add multiple items at once. It's more efficient than adding items one by one and makes your code cleaner. Any keys that already exist get updated; new keys get added.
Iterating Through Dictionaries - Accessing Keys and Values
When working with dictionaries, you often need to process all items. Python provides several methods to iterate through dictionaries, each giving you different information.
student = {
"name": "Alice",
"age": 20,
"major": "CS",
"gpa": 3.8
}
# Iterate over keys (default behavior)
print("Keys:")
for key in student:
print(f" {key}")
# Iterate over keys explicitly
print("\nKeys (explicit):")
for key in student.keys():
print(f" {key}: {student[key]}")
# Iterate over values
print("\nValues:")
for value in student.values():
print(f" {value}")
# Iterate over key-value pairs (most useful!)
print("\nKey-Value Pairs:")
for key, value in student.items():
print(f" {key}: {value}")
# List all keys, values, or items
print(f"\nAll keys: {list(student.keys())}")
print(f"All values: {list(student.values())}")
print(f"All items: {list(student.items())}")
items() is Your Friend: In most cases, items() is what you want. It gives you both keys and values in one elegant iteration. This is the most Pythonic way to process dictionary data and you'll see it constantly in professional code.
Real-World Application - Configuration Management: Applications often use dictionaries for configuration: database settings, API keys, feature flags. When loading configuration, you iterate through items() to set each configuration value, validating as you go. When displaying configuration to admins, you iterate through items() to show current settings in a readable format.
Dictionary Comprehensions - Elegant Data Transformation
Just like list comprehensions, Python supports dictionary comprehensions for creating dictionaries elegantly and concisely. This is one of Python's most powerful features for data transformation.
# Create dictionary of squares
squares = {x: x**2 for x in range(1, 6)}
print(f"Squares: {squares}")
# From two lists
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
people = {name: age for name, age in zip(names, ages)}
print(f"People: {people}")
# With condition - only even numbers
evens = {x: x**2 for x in range(10) if x % 2 == 0}
print(f"Even squares: {evens}")
# Transform existing dictionary
prices = {"apple": 0.50, "banana": 0.30, "orange": 0.70}
# Add 10% tax
taxed = {item: price * 1.10 for item, price in prices.items()}
print(f"With tax: {taxed}")
# Filter dictionary
students = {"Alice": 85, "Bob": 92, "Charlie": 78, "Diana": 95}
high_scorers = {name: score for name, score in students.items() if score >= 90}
print(f"High scorers: {high_scorers}")
When to Use Comprehensions: Dictionary comprehensions are perfect for transforming data: converting units, applying calculations, filtering based on conditions, or restructuring data. They're more readable and often faster than traditional loops. However, if the logic gets complex, a regular loop might be clearer.
Understanding Sets - Collections of Unique Elements
Now let's explore sets - Python's data structure for storing unique, unordered elements. Sets automatically eliminate duplicates and provide ultra-fast membership testing. While less commonly used than lists or dictionaries, sets are invaluable for specific problems involving uniqueness and set operations.
What Makes Sets Special: Sets enforce uniqueness - each element can appear only once. If you try to add a duplicate, it's silently ignored. This property makes sets perfect for scenarios where you need to ensure no duplicates exist or when you want to find unique items in a collection.
Real-World Analogy: Think of a set like attendees at an event. Each person is either there or not - being there twice doesn't make sense. If someone tries to check in twice, the second attempt is ignored. Sets model this "either in or not in" relationship perfectly.
Speed Advantage: Like dictionaries, sets use hash tables internally, making membership testing (checking if an item exists) incredibly fast regardless of set size. Checking if an item is in a list of 1 million items could take a while; checking in a set is instant.
Creating and Using Sets
# Creating sets with curly braces
fruits = {"apple", "banana", "orange"}
print(f"Fruits set: {fruits}")
# Duplicates are automatically removed
numbers = {1, 2, 3, 2, 1, 4, 3, 5}
print(f"Numbers set: {numbers}") # {1, 2, 3, 4, 5}
# Creating set from list (removes duplicates)
list_with_dupes = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
unique_numbers = set(list_with_dupes)
print(f"Unique: {unique_numbers}")
# Empty set (must use set(), not {})
empty = set() # {} creates empty dict, not set!
print(f"Empty set: {empty}")
# Set from string (unique characters)
letters = set("hello")
print(f"Unique letters: {letters}")
Critical Syntax Note: Empty curly braces {} create an empty dictionary, not an empty set! Always use set() to create an empty set. This is a common source of bugs for beginners.
Set Elements Must Be Immutable: Like dictionary keys, set elements must be immutable (strings, numbers, tuples). You cannot add lists or dictionaries to sets because they can change. This is required for sets to maintain their speed advantages.
Set Operations - Adding and Removing Elements
colors = {"red", "blue", "green"}
print(f"Original: {colors}")
# add() - adds single element
colors.add("yellow")
print(f"After add: {colors}")
# add() ignores duplicates
colors.add("red") # Already exists, ignored
print(f"After duplicate add: {colors}")
# update() - adds multiple elements
colors.update(["purple", "orange", "pink"])
print(f"After update: {colors}")
# remove() - removes element (error if not found)
colors.remove("blue")
print(f"After remove: {colors}")
# discard() - removes element (no error if not found)
colors.discard("blue") # Already removed, but no error
colors.discard("black") # Doesn't exist, but no error
print(f"After discard: {colors}")
# pop() - removes and returns arbitrary element
removed = colors.pop()
print(f"Popped: {removed}, Set now: {colors}")
# clear() - removes all elements
colors.clear()
print(f"After clear: {colors}")
remove() vs discard(): Both remove elements, but behavior differs when the element doesn't exist. remove() raises an error (use when the element must exist), discard() does nothing (use when you're not sure if it exists). Choose based on whether missing elements indicate a bug or are expected.
Set Mathematical Operations - Powerful Set Theory
Sets support mathematical set operations like union, intersection, and difference. These operations are incredibly useful for comparing collections, finding commonalities, identifying unique items, and solving many real-world problems elegantly.
set1 = {1, 2, 3, 4, 5}
set2 = {4, 5, 6, 7, 8}
# Union - all unique elements from both sets
union = set1 | set2
print(f"Union: {union}")
# Or: union = set1.union(set2)
# Intersection - elements in both sets
intersection = set1 & set2
print(f"Intersection: {intersection}")
# Or: intersection = set1.intersection(set2)
# Difference - elements in set1 but not set2
difference = set1 - set2
print(f"Set1 - Set2: {difference}")
# Or: difference = set1.difference(set2)
# Symmetric difference - elements in either set but not both
sym_diff = set1 ^ set2
print(f"Symmetric difference: {sym_diff}")
# Or: sym_diff = set1.symmetric_difference(set2)
# Subset and superset checks
small = {1, 2}
large = {1, 2, 3, 4, 5}
print(f"{small} is subset of {large}: {small.issubset(large)}")
print(f"{large} is superset of {small}: {large.issuperset(small)}")
Real-World Application - User Permissions System: Imagine an application with user roles. Admin role has permissions {"read", "write", "delete", "admin"}. Editor role has {"read", "write"}. To check what permissions admins have that editors don't: admin_permissions - editor_permissions. To find common permissions: admin_permissions & editor_permissions. Set operations make permission management elegant and efficient.
Practical Real-World Examples
# Analyze text for unique words
text = """Python is amazing. Python is powerful.
Python is the best programming language for beginners."""
# Convert to lowercase and split into words
words = text.lower().split()
# Get unique words using set
unique_words = set(words)
print(f"Total words: {len(words)}")
print(f"Unique words: {len(unique_words)}")
print(f"Unique word list: {sorted(unique_words)}")
# Find words that appear more than once
word_counts = {}
for word in words:
word_counts[word] = word_counts.get(word, 0) + 1
repeated = {word for word, count in word_counts.items() if count > 1}
print(f"Repeated words: {repeated}")
# Simple friend suggestion system
user_friends = {
"Alice": {"Bob", "Charlie", "Diana"},
"Bob": {"Alice", "Eve", "Frank"},
"Charlie": {"Alice", "Diana", "George"}
}
# Find potential friends for Alice
# (friends of friends who aren't already friends)
alice_friends = user_friends["Alice"]
friends_of_friends = set()
for friend in alice_friends:
if friend in user_friends:
friends_of_friends.update(user_friends[friend])
# Remove Alice herself and her existing friends
suggestions = friends_of_friends - alice_friends - {"Alice"}
print(f"Friend suggestions for Alice: {suggestions}")
# E-commerce inventory system
inventory = {
"laptop": {"price": 999, "stock": 15, "category": "electronics"},
"mouse": {"price": 25, "stock": 50, "category": "electronics"},
"desk": {"price": 299, "stock": 8, "category": "furniture"},
"chair": {"price": 199, "stock": 12, "category": "furniture"},
"monitor": {"price": 349, "stock": 0, "category": "electronics"}
}
# Find out-of-stock items
out_of_stock = [name for name, data in inventory.items()
if data["stock"] == 0]
print(f"Out of stock: {out_of_stock}")
# Calculate total inventory value
total_value = sum(data["price"] * data["stock"]
for data in inventory.values())
print(f"Total inventory value: ${total_value:,.2f}")
# Get all categories (unique)
categories = {data["category"] for data in inventory.values()}
print(f"Categories: {categories}")
# Items by category
for category in categories:
items = [name for name, data in inventory.items()
if data["category"] == category]
print(f"{category.title()}: {', '.join(items)}")
Nested Dictionaries - Complex Data Structures
Real-world applications often require complex, nested data structures. Dictionaries can contain other dictionaries, creating hierarchical data models that mirror real-world relationships.
# School database with nested structure
school = {
"students": {
"S001": {
"name": "Alice",
"grade": 10,
"subjects": ["Math", "Science", "English"],
"scores": {"Math": 95, "Science": 88, "English": 92}
},
"S002": {
"name": "Bob",
"grade": 11,
"subjects": ["Math", "Physics", "Chemistry"],
"scores": {"Math": 87, "Physics": 91, "Chemistry": 85}
}
},
"teachers": {
"T001": {"name": "Mr. Smith", "subject": "Math", "experience": 15},
"T002": {"name": "Ms. Johnson", "subject": "Science", "experience": 10}
}
}
# Accessing nested data
alice = school["students"]["S001"]
print(f"Student: {alice['name']}")
print(f"Math score: {alice['scores']['Math']}")
# Calculate average score for a student
alice_scores = alice["scores"].values()
alice_avg = sum(alice_scores) / len(alice_scores)
print(f"Alice's average: {alice_avg:.2f}")
# Find all students taking Math
math_students = [
data["name"] for data in school["students"].values()
if "Math" in data["subjects"]
]
print(f"Math students: {math_students}")
When to Use Which Data Structure
Choosing the right data structure is crucial for writing efficient, maintainable code. Here's a comprehensive guide:
Use Lists When:
• Order matters and you need to maintain sequence
• You need to access items by position
• Duplicates are allowed and meaningful
• You'll iterate through all items frequently
• Examples: Order history, playlist, message threads
Use Dictionaries When:
• You need to look up values by meaningful keys
• Relationships between data matter (key maps to value)
• Fast lookup by key is important
• Data is naturally key-value paired
• Examples: User profiles, configuration, database records
Use Sets When:
• Uniqueness is required (no duplicates)
• Fast membership testing needed ("is X in collection?")
• Set operations (union, intersection) are useful
• Order doesn't matter
• Examples: Tags, unique visitors, permissions, categories
Use Tuples When:
• Data is fixed and won't change
• Returning multiple values from functions
• Using as dictionary keys
• Examples: Coordinates, RGB colors, database rows
Summary and Best Practices
You've mastered two powerful data structures that are essential for professional Python programming:
✓ Dictionaries for fast key-value lookups and relationships
✓ Sets for uniqueness and set operations
✓ Nested structures for complex data models
✓ Choosing the right data structure for each problem
✓ Real-world applications and patterns
Key Takeaways:
1. Use Meaningful Keys: Dictionary keys should clearly describe the data they map to.
2. Handle Missing Keys Gracefully: Use get() with defaults for optional data.
3. Leverage Set Operations: Don't reinvent the wheel - use union, intersection, etc.
4. Choose the Right Structure: Consider access patterns, uniqueness needs, and ordering requirements.
5. Keep It Simple: Don't over-nest dictionaries. If your data structure gets too complex, consider using classes (object-oriented programming).
Practice Challenge: Build a simple library management system using dictionaries and sets. Store books with ISBNs as keys, track borrowed books in a set, implement features to add/remove books, check availability, and find books by author or category. This exercise combines everything you've learned about these data structures!
Looking Ahead: Now that you understand Python's core data structures (lists, tuples, dictionaries, sets), you're ready for more advanced topics like file handling, working with external data, error handling, and object-oriented programming. These data structures will be the foundation for everything you build in Python!

