Programming with Python | Chapter 7: Data Structures – Dictionaries and Sets

Chapter Objectives

  • Understand and create dictionaries (dict) for storing key-value pairs in Python.
  • Access, add, modify, and remove items from dictionaries using keys.
  • Iterate over dictionary keys, values, and key-value pairs.
  • Use common dictionary methods like .keys(), .values(), .items(), .get(), and .update().
  • Understand the properties of dictionary keys (unique, immutable).
  • Understand and create sets (set) for storing unordered collections of unique items.
  • Perform common set operations: adding/removing elements, union, intersection, difference, symmetric difference.
  • Use common set methods like .add(), .remove(), .discard(), .pop(), .union(), .intersection(), etc.
  • Recognize appropriate use cases for dictionaries and sets.

Introduction

Chapters 5 introduced lists and tuples, which are ordered sequences accessed by numerical index. This chapter introduces two non-sequential built-in data structures: dictionaries and sets. Dictionaries store data as key-value pairs, allowing you to look up a value using a unique key (like looking up a word in a real dictionary). Sets store an unordered collection of unique items, useful for tasks like removing duplicates or performing mathematical set operations (like finding common elements between two groups).

Theory & Explanation

Dictionaries (dict): Key-Value Mappings

A dictionary is an unordered collection of items where each item consists of a unique key and an associated value. Dictionaries are enclosed in curly braces {}. They are incredibly useful for associating related pieces of data.

  • Key-Value Pairs: Data is stored as key: value.
  • Keys must be unique: You cannot have duplicate keys in a dictionary.
  • Keys must be immutable: Keys must be of an immutable type (like strings, numbers, or tuples containing only immutable elements). Lists cannot be used as keys.
  • Values can be anything: Values can be of any data type (numbers, strings, lists, other dictionaries, etc.) and can be duplicated.
  • Unordered (Historically): Before Python 3.7, dictionaries were unordered. From Python 3.7 onwards, dictionaries remember the insertion order, but you still access items by key, not by numerical index.
  • Mutable: Dictionaries can be changed after creation (add, remove, modify key-value pairs).

Creating Dictionaries:

Python
empty_dict = {}
student = {
    "name": "Alice",
    "age": 21,
    "major": "Computer Science",
    "courses": ["CS101", "MA203", "PH101"] # Value can be a list
}
# Using the dict() constructor
config = dict(host="localhost", port=8080, debug=True)

print(student)
print(config)
print(type(student)) # <class 'dict'>

Accessing Values:

You access the value associated with a key using square bracket notation dictionary[key]. If the key doesn’t exist, this raises a KeyError.

Dictionary Key-Value Pairs

Dictionary Key-Value Mapping Shows keys mapping to values in a dictionary. student = {“name”: “Alice”, “age”: 21} “name” “Alice” “age” 21

Each unique, immutable key points to its associated value.

Python
print(f"Student Name: {student['name']}")   # Alice
print(f"Student Age: {student['age']}")     # 21
# print(student['gpa']) # This would raise a KeyError

# Using the .get() method (safer access)
# .get(key, default=None) returns the value for key if it exists,
# otherwise returns the default value (None if not specified). No KeyError!
major = student.get("major")
gpa = student.get("gpa", "N/A") # Provide a default value if 'gpa' key is missing

print(f"Student Major: {major}") # Computer Science
print(f"Student GPA: {gpa}")     # N/A

graph TD
    subgraph Accessing 'my_dict'
        A[Start] --> B{Key exists in my_dict?};
        B -- Yes --> C["Use my_dict[key]"];
        C --> D[Return value];
        B -- No --> E["Use my_dict[key]"];
        E --> F[Raise KeyError!];

        G[Start] --> H{"Use my_dict.get(key, default)"};
        H --> I{Key exists in my_dict?};
        I -- Yes --> J[Return value];
        I -- No --> K["Return default value (e.g., None)"];
        J --> L[End];
        K --> L;
    end

    style F fill:#f99,stroke:#f00,stroke-width:2px
    style K fill:#cfc,stroke:#080,stroke-width:1px

Adding and Modifying Items:

You can add a new key-value pair or modify the value of an existing key using assignment with square bracket notation.

Python
# Add a new key-value pair
student["email"] = "alice@example.com"
print(student)

# Modify an existing value
student["age"] = 22
print(student)

graph TD
    A["Start: Assign dict[key] = value"] --> B{Does 'key' already exist in dict?};
    B -- Yes --> C[Update existing value associated with 'key' to new 'value'];
    B -- No --> D[Add new pair: 'key': 'value' to dict];
    C --> E[End];
    D --> E;

Removing Items:

  • del dictionary[key]: Removes the key-value pair with the specified key. Raises KeyError if the key doesn’t exist.
  • .pop(key, default=None): Removes the item with the specified key and returns its value. If the key is not found, it returns the default value (or raises KeyError if no default is given).
  • .popitem(): Removes and returns an arbitrary (key, value) pair (in LIFO order for Python 3.7+). Raises KeyError if the dictionary is empty.
Python
# Remove 'major' using del
del student["major"]
print(student)

# Remove 'age' using pop and get its value
removed_age = student.pop("age")
print(f"Removed age: {removed_age}")
print(student)

# Remove an arbitrary item (last inserted in 3.7+)
key, value = student.popitem()
print(f"Removed item: {key}: {value}")
print(student)

Iterating Over Dictionaries:

There are several ways to loop through dictionaries:

Python
student = {"name": "Bob", "id": 123, "dept": "Physics"}

# 1. Iterate over keys (default behavior)
print("\nIterating over keys:")
for k in student: # Equivalent to 'for k in student.keys():'
    print(f"Key: {k}, Value: {student[k]}")

# 2. Iterate over values
print("\nIterating over values:")
for v in student.values():
    print(f"Value: {v}")

# 3. Iterate over key-value pairs (items)
print("\nIterating over items (key-value pairs):")
for k, v in student.items(): # .items() returns view objects containing (key, value) tuples
    print(f"Key: {k}, Value: {v}")

Other Common Dictionary Methods:

Common Dictionary Methods & Operations

Method / Operation Description Example (d = {'a': 1, 'b': 2}) Return Value / Result
d[key] Accesses the value associated with key. d['a'] 1
(Raises KeyError if key not found)
d.get(key, default) Accesses value for key. Returns default if key not found (None if default omitted). d.get('b')
d.get('c', 0)
2
0
(No KeyError)
d[key] = value Adds a new key-value pair or updates the value if key exists. d['c'] = 3
d['a'] = 5
d becomes {'a': 5, 'b': 2, 'c': 3}
del d[key] Removes the item with the specified key. del d['a'] d becomes {'b': 2}
(Raises KeyError if key not found)
d.pop(key, default) Removes item with key and returns its value. Returns default if key not found (raises KeyError if no default). d.pop('b')
d.pop('c', 0)
Returns 2, d becomes {'a': 1}
Returns 0 (if ‘c’ not in d)
d.popitem() Removes and returns an arbitrary (key, value) pair (LIFO in Python 3.7+). d.popitem() Returns ('b', 2), d becomes {'a': 1}
(Raises KeyError if empty)
len(d) Returns the number of key-value pairs (items). len(d) 2
key in d Checks if key exists in the dictionary’s keys. 'a' in d
'z' in d
True
False
d.keys() Returns a view object containing the dictionary’s keys. d.keys() dict_keys(['a', 'b'])
d.values() Returns a view object containing the dictionary’s values. d.values() dict_values([1, 2])
d.items() Returns a view object containing the dictionary’s (key, value) tuple pairs. d.items() dict_items([('a', 1), ('b', 2)])
d.update(other_dict) Updates d with key-value pairs from other_dict, overwriting existing keys. d.update({'b': 3, 'c': 4}) d becomes {'a': 1, 'b': 3, 'c': 4}
d.clear() Removes all items from the dictionary. d.clear() d becomes {}

Dictionaries are mutable mappings optimized for key-based lookups.

Sets (set): Unordered Collections of Unique Items

A set is an unordered collection of unique, immutable items. Sets are enclosed in curly braces {} like dictionaries, but they contain individual items separated by commas, not key-value pairs.

  • Unordered: Items have no specific index or position.
  • Unique: Sets automatically discard duplicate items.
  • Mutable: You can add or remove items from a set after creation.
  • Items must be immutable: Set elements must be of immutable types (numbers, strings, tuples). You cannot have lists or dictionaries as set elements.

Creating Sets:

Python
empty_set = set() # IMPORTANT: {} creates an empty dictionary, not a set!
numbers = {1, 2, 3, 4, 4, 5} # Duplicates are automatically removed
mixed_set = {1.0, "hello", (1, 2)} # Can contain different immutable types
chars = set("hello world") # Creates a set from characters in the string

print(empty_set)
print(numbers)      # {1, 2, 3, 4, 5}
print(mixed_set)
print(chars)        # {'l', 'o', 'h', 'd', ' ', 'w', 'r', 'e'} (order may vary)
print(type(numbers)) # <class 'set'>

Adding and Removing Items:

  • .add(element): Adds a single element to the set. If the element is already present, the set remains unchanged.
  • .update(iterable): Adds all elements from an iterable (like a list, tuple, or another set) to the set.
  • .remove(element): Removes the specified element. Raises KeyError if the element is not found.
  • .discard(element): Removes the specified element if it is present. Does not raise an error if the element is not found (safer than .remove()).
  • .pop(): Removes and returns an arbitrary element from the set (since sets are unordered). Raises KeyError if the set is empty.
  • .clear(): Removes all elements from the set.
Python
colors = {"red", "green", "blue"}
print(f"Initial colors: {colors}")

colors.add("yellow")
print(f"After add: {colors}")

colors.update(["orange", "purple", "red"]) # 'red' is ignored as it's a duplicate
print(f"After update: {colors}")

colors.remove("green")
print(f"After remove 'green': {colors}")
# colors.remove("black") # This would raise KeyError

colors.discard("blue")
print(f"After discard 'blue': {colors}")
colors.discard("black") # No error raised
print(f"After discard 'black': {colors}")

removed_color = colors.pop()
print(f"Popped color: {removed_color}")
print(f"After pop: {colors}")

Set Operations:

Sets support powerful mathematical operations.

Python
set_a = {1, 2, 3, 4}
set_b = {3, 4, 5, 6}

# Union
union_set = set_a | set_b
print(f"Union: {union_set}") # {1, 2, 3, 4, 5, 6}

# Intersection
intersection_set = set_a & set_b
print(f"Intersection: {intersection_set}") # {3, 4}

# Difference (A - B)
difference_set = set_a - set_b
print(f"Difference (A - B): {difference_set}") # {1, 2}

# Symmetric Difference
sym_diff_set = set_a ^ set_b
print(f"Symmetric Difference: {sym_diff_set}") # {1, 2, 5, 6}

Union Operation:

graph TD
    A[Set A] --> C{Combine All Elements};
    B[Set B] --> C;
    C --> D{Remove Duplicates};
    D --> E[Result: Union Set];

    style E fill:#ccf,stroke:#33a,stroke-width:1px

Intersection Operation

graph TD
    A[Set A] --> C{Find Common Elements};
    B[Set B] --> C;
    C --> D[Result: Intersection Set];

    style D fill:#cfc,stroke:#080,stroke-width:1px

Set Intersection (A & B)

Set Intersection Venn diagram showing the intersection of two sets. A = {1, 2, 3}, B = {3, 4, 5} 1 2 Set A 4 5 Set B 3 Intersection (A & B) = {3}

The intersection contains only elements present in both sets.

Difference Operation:

graph TD
    A[Set A] --> C{Keep elements from A...};
    B[Set B] --> D{...that are NOT in B};
    C --> D;
    D --> E["Result: Difference Set (A - B)"];

    style E fill:#fcc,stroke:#a33,stroke-width:1px

Symmetric Difference Operation:

graph TD
    A[Set A] --> C{"Find elements ONLY in A (A - B)"};
    B[Set B] --> D{"Find elements ONLY in B (B - A)"};
    C --> E{Combine Unique Elements};
    D --> E;
    E --> F[Result: Symmetric Difference Set];

    style F fill:#ffc,stroke:#aa3,stroke-width:1px

Other Common Set Methods/Operations:

Common Set Methods & Operations

Method / Operator Description Example (s={1, 2}, s2={2, 3}) Result / Return Value
s.add(elem) Adds elem to the set. No effect if already present. s.add(3)
s.add(2)
s becomes {1, 2, 3}
s remains {1, 2}
s.update(iterable) Adds all elements from iterable to the set. s.update([3, 4]) s becomes {1, 2, 3, 4}
s.remove(elem) Removes elem from the set. s.remove(2) s becomes {1}
(Raises KeyError if elem not found)
s.discard(elem) Removes elem if present. Does nothing if not found. s.discard(1)
s.discard(5)
s becomes {2}
s remains {1, 2}
(No KeyError)
s.pop() Removes and returns an arbitrary element. s.pop() Returns 1 or 2, s has one element left.
(Raises KeyError if empty)
s.clear() Removes all elements from the set. s.clear() s becomes set()
len(s) Returns the number of elements in the set. len(s) 2
elem in s Checks if elem is present in the set. 1 in s
5 in s
True
False
s | s2
s.union(s2)
Returns a new set with elements from both sets. s | s2 {1, 2, 3}
s & s2
s.intersection(s2)
Returns a new set with elements common to both sets. s & s2 {2}
s - s2
s.difference(s2)
Returns a new set with elements in s but not in s2. s - s2 {1}
s ^ s2
s.symmetric_difference(s2)
Returns a new set with elements in either set, but not both. s ^ s2 {1, 3}
s1 <= s2
s1.issubset(s2)
Checks if s1 is a subset of s2. {1}.issubset(s) True
s1 >= s2
s1.issuperset(s2)
Checks if s1 is a superset of s2. s.issuperset({1}) True
s1.isdisjoint(s2) Checks if s1 and s2 have no elements in common. s.isdisjoint({4, 5}) True

Sets are mutable, unordered collections of unique, immutable items, useful for membership testing and mathematical set operations.

Why Use Sets?

  1. Removing Duplicates: Quickly get unique items from a list: unique_items = set(my_list).
  2. Membership Testing: Checking if an item exists in a set (item in my_set) is very fast (much faster than checking in a list for large collections).
  3. Set Operations: Performing unions, intersections, differences efficiently.

Code Examples

set Animation (Uniqueness)

Current Set Contents:

Enter an item and click 'Add to Set'.

Example 1: Dictionary for Word Counting

Python
# word_counter.py

text = "this is a sample text this text has sample words"
word_counts = {} # Initialize an empty dictionary

words = text.lower().split() # Convert to lowercase and split into words

for word in words:
    if word in word_counts:
        word_counts[word] += 1 # Increment count if word exists
    else:
        word_counts[word] = 1 # Add word with count 1 if new

print("Word Counts:")
# Print sorted by word
for word, count in sorted(word_counts.items()): # .items() gets pairs, sorted() sorts them by key
    print(f"- {word}: {count}")

# Find the most frequent word
most_frequent_word = ""
max_count = 0
for word, count in word_counts.items():
    if count > max_count:
        max_count = count
        most_frequent_word = word

print(f"\nMost frequent word: '{most_frequent_word}' (appeared {max_count} times)")

Explanation:

  • We initialize an empty dictionary word_counts.
  • The text is converted to lowercase and split into a list of words.
  • The for loop iterates through the words list.
  • Inside the loop, we check if the word is already a key in word_counts.
    • If yes, we increment its associated value (count).
    • If no, we add the word as a new key with a value of 1.
  • Finally, we iterate through the .items() of the completed dictionary to print the counts and find the most frequent word. sorted(word_counts.items()) sorts the key-value pairs alphabetically by key before printing.

Example 2: Using Sets for Finding Common Elements

Python
# common_elements.py

group_a_skills = {"Python", "Git", "HTML", "CSS", "SQL"}
group_b_skills = {"Java", "Git", "SQL", "Testing", "Docker"}

# Find skills common to both groups
common_skills = group_a_skills.intersection(group_b_skills)
# Alternative: common_skills = group_a_skills & group_b_skills
print(f"Common skills: {common_skills}")

# Find skills unique to group A
unique_to_a = group_a_skills.difference(group_b_skills)
# Alternative: unique_to_a = group_a_skills - group_b_skills
print(f"Skills unique to Group A: {unique_to_a}")

# Find skills unique to group B
unique_to_b = group_b_skills.difference(group_a_skills)
# Alternative: unique_to_b = group_b_skills - group_a_skills
print(f"Skills unique to Group B: {unique_to_b}")

# Find all unique skills across both groups
all_skills = group_a_skills.union(group_b_skills)
# Alternative: all_skills = group_a_skills | group_b_skills
print(f"All unique skills: {all_skills}")

# Check if one group's skills are a subset of another (example)
required_skills = {"Python", "Git"}
print(f"Group A has all required skills? {required_skills.issubset(group_a_skills)}") # True

Explanation:

  • Two sets, group_a_skills and group_b_skills, are created.
  • Set methods (.intersection(), .difference(), .union()) and operators (&, -, |) are used to find common skills, unique skills, and all skills combined.
  • .issubset() checks if one set is contained within another.

Common Mistakes or Pitfalls

  • KeyError (Dictionaries): Accessing a dictionary key that doesn't exist using my_dict[key]. Use my_dict.get(key) or check with if key in my_dict: first.
  • Using Mutable Keys (Dictionaries): Trying to use a list or another mutable type as a dictionary key will raise a TypeError. Keys must be immutable.
  • {} Creates Empty Dictionary: Using {} creates an empty dictionary, not an empty set. Use set() to create an empty set.
  • Sets are Unordered: Relying on the order of elements in a set. While insertion order might be preserved in some Python versions for some operations, it's not guaranteed and should not be relied upon.
  • KeyError vs No Error (Sets): Using my_set.remove(element) for an element that might not exist raises KeyError. Use my_set.discard(element) if you don't want an error in that case.
  • Modifying While Iterating: Modifying dictionaries or sets while iterating over them directly can sometimes lead to RuntimeError or unexpected behavior. It's often safer to iterate over a copy of the keys/items or collect changes and apply them after the loop.

Chapter Summary

  • Dictionaries (dict) store unordered (insertion order preserved since 3.7) collections of key-value pairs.
    • Created with {key: value, ...} or dict().
    • Accessed via my_dict[key] (raises KeyError) or my_dict.get(key, default).
    • Keys must be unique and immutable; values can be anything.
    • Mutable: items can be added, modified (my_dict[key] = value), or removed (del, .pop(), .popitem()).
    • Iterate using .keys(), .values(), .items().
  • Sets (set) store unordered collections of unique, immutable items.
    • Created with {item1, item2, ...} (if not empty) or set().
    • Automatically handle uniqueness. Items must be immutable.
    • Mutable: items can be added (.add(), .update()) or removed (.remove(), .discard(), .pop()).
    • Support fast membership testing (item in my_set).
    • Support mathematical operations: union (|), intersection (&), difference (-), symmetric difference (^).

Exercises & Mini Projects

Exercises

  1. Dictionary Creation and Access: Create a dictionary representing a book with keys "title", "author", and "year". Print the author's name. Try accessing the "ISBN" key using .get() with a default value.
  2. Dictionary Modification: Start with the book dictionary from Exercise 1. Add a new key "genre" with an appropriate value. Change the "year" to a different value. Remove the "author" key using .pop(). Print the final dictionary.
  3. Set Creation and Uniqueness: Create a list with duplicate numbers, e.g., [1, 2, 5, 2, 3, 4, 5, 1]. Create a set from this list to get the unique numbers. Print the resulting set.
  4. Set Operations: Create two sets: set1 = {10, 20, 30, 40} and set2 = {30, 40, 50, 60}. Calculate and print their union, intersection, and the difference (set1 - set2).
  5. Dictionary Iteration: Create a dictionary mapping country names (strings) to their capitals (strings). Iterate through the dictionary using .items() and print each country and its capital in the format "The capital of [Country] is [Capital]."

Mini Project: Contact Book

Goal: Create a simple command-line contact book using a dictionary.

Steps:

  1. Initialize an empty dictionary called contacts. This dictionary will store names (keys) and phone numbers (values), both as strings.
  2. Use a while True: loop for the main menu (similar to the To-Do List project).
  3. Provide options:
    • 1: Add/Update Contact
    • 2: View Contact
    • 3: Delete Contact
    • 4: View All Contacts
    • 5: Exit
  4. Implement the choices using if-elif-else:
    • Choice 1 (Add/Update):
      • Ask for the contact's name (key).
      • Ask for the contact's phone number (value).
      • Add or update the entry in the contacts dictionary: contacts[name] = phone_number. Print a confirmation.
    • Choice 2 (View):
      • Ask for the name to view.
      • Use .get(name) to retrieve the phone number.
      • If .get() returns a value (not None), print the name and number.
      • If .get() returns None (or your chosen default), print "Contact not found."
    • Choice 3 (Delete):
      • Ask for the name to delete.
      • Check if the name exists as a key (if name in contacts:).
      • If it exists, use del contacts[name] or contacts.pop(name) to remove it. Print a confirmation.
      • If it doesn't exist, print "Contact not found."
    • Choice 4 (View All):
      • Check if the dictionary is empty (if not contacts:). If so, print "Contact book is empty."
      • If not empty, print "--- All Contacts ---".
      • Iterate through the dictionary using .items() and print each name and phone number.
    • Choice 5 (Exit):
      • Print "Exiting contact book."
      • Use break to exit the while True: loop.
    • Else (Invalid Choice):
      • Print an error message.

Additional Sources:

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top