Lecture 05: Exploration of Dictionaries and Sets
Welcome to Lecture 05! In our previous lecture, we mastered Python lists and for loops, building a To-Do List application. You learned how to store ordered collections of items and iterate through them. Today, we’re going to explore two more powerful Python data structures: Dictionaries and Sets. We’ll understand their unique properties and how they can be incredibly efficient for specific tasks. And, true to our style, we’ll learn about them by working through practical mini-projects! We’ll also get a tiny peek into a concept called “hashing,” which is the secret sauce that makes dictionaries and sets so fast for certain operations.
Goals for this lecture:
- Understand the concept of key-value pairs and how dictionaries use them for efficient data storage and retrieval.
- Learn to create, access, modify, and loop through dictionaries in Python.
- Apply this knowledge by building a Word Frequency Counter project.
- Understand the concept of sets for storing collections of unique, unordered items.
- Learn to perform common and powerful set operations like union, intersection, and difference.
- Apply set knowledge by building a Unique Class Enrollment Checker project.
- Briefly touch upon the idea of hashing and appreciate its role in the performance of dictionaries and sets.
- Clearly compare and contrast lists, dictionaries, and sets to understand when to choose which data structure for a given problem.
Recap from Previous Lectures
Let’s quickly recall some key data structures and concepts:
- Lists: We learned that lists (e.g.,
my_classmates = ["Priya", "Rohan"]) are ordered collections, meaning items are stored in a specific sequence. They are indexed by number (e.g.,my_classmates[0]gives “Priya”), and they are mutable, meaning we can change their content after creation (add, remove, or modify items). Lists are fantastic when the order of items is important or when you need to access items by their numerical position.
Today, dictionaries and sets will show us different ways to organize data, each with unique advantages.
Conceptual Introduction to Hashing (The Magic Behind Speed)
Before we dive into the specifics of dictionaries and sets, let’s briefly discuss a clever idea that makes them work so efficiently for many tasks: hashing. You don’t need to know the complex math behind it, but understanding the concept is helpful.
Analogy: The Magical Filing Cabinet with a Super-Smart Assistant
Imagine you have a massive filing cabinet with thousands of drawers, and a super-smart assistant.
- Storing: When you want to store a new piece of information (say, Rohan’s phone number), you give the assistant a unique “label” for that information (e.g., Rohan’s name – this label is like a key). The assistant instantly performs a quick calculation (this is the “hashing function”) on Rohan’s name, and this calculation tells them exactly which drawer and which specific folder (memory location or “bucket”) Rohan’s phone number should go into. They don’t have to look through all the drawers one by one to find an empty spot; they know where it goes almost immediately.
- Retrieving: Later, when you need Rohan’s phone number, you just tell the assistant “Rohan.” The assistant performs the same quick calculation on “Rohan,” instantly knows which drawer and folder to look in, and POOF! – they hand you the correct phone number. Again, no need to search every drawer.
How it (Sort of) Works (The “Magic” Simplified):
In programming, hashing uses a special mathematical function called a “hash function.”
- When you want to store something (like a word and its count in a dictionary, or a student’s name in a set), Python takes the “label” (the key in dictionaries, or the item itself in sets) and feeds it to this hash function.
- The hash function processes this key and produces a special, usually numerical, value called a hash code (or simply, a hash). This hash code is designed to be fairly unique for unique keys (though sometimes “collisions” can happen, which Python handles internally).
- This hash code is then used, often with another quick calculation, to determine a specific “slot” or location in the computer’s memory where the data (or a reference to it) will be stored.
The Benefit: Incredible Speed!
Because of hashing, when you want to:
- Find a value associated with a key in a dictionary (e.g., “What’s the count for the word ‘python’?”).
- Check if an item exists in a set (e.g., “Is ‘Priya’ a member of the Coding Club?”).
- Add a new item to a dictionary or set.
- Delete an item from a dictionary or set (using its key or the item itself).
Python can use the key (or the item) to quickly calculate its hash code and go almost directly to the correct memory location. This means these operations are usually very, very fast, often taking roughly the same small amount of time regardless of how many items are in the dictionary or set. We call this “average constant time complexity” or O(1) in computer science terms.
This is a huge advantage over lists. If you have a long list and want to find if a specific item is in it (e.g., if "apple" in my_long_fruit_list:), Python might have to look at each item one by one from the beginning until it finds “apple” or reaches the end. If the list has a million items, that could take a million checks in the worst case! With dictionaries and sets, finding “apple” (if “apple” is a key or a set member) is typically much faster, almost instantaneous, due to hashing.
Both Dictionaries and Sets in Python use this powerful hashing mechanism “under the hood” to provide this speed advantage for their core operations like adding, deleting, and checking for items. Keep this idea of “fast lookups and checks” in mind as we explore them.
Project 1: Word Frequency Counter (Focus on Dictionaries)
Our first project will use dictionaries to solve a common text analysis problem, which will clearly illustrate their power.
Problem Statement:
Imagine you are given a long piece of text – it could be a chapter from a book, the lyrics of a song, a news article, or a speech. Your task is to write a Python program that counts how many times each unique word appears in that text. For example, if the input text is “the quick brown fox jumps over the lazy dog”, your program should be able to tell you that “the” appeared 2 times, “quick” 1 time, “brown” 1 time, and so on for all the words.
Trying to do this efficiently using only lists would be quite complicated and slow. You might think of keeping one list of all the unique words you’ve found and another parallel list to store their counts. But then, every time you see a word, you’d have to search the first list to see if you’ve seen it before, find its position, and then update the count at the same position in the second list. This becomes very inefficient for large texts. This is a perfect scenario for a Python dictionary!
Introducing Dictionaries for this Task
A dictionary in Python is a versatile and widely used data structure that stores a collection of items. Unlike lists which are ordered sequences of single items, a dictionary stores items as key-value pairs.
- Key: A key is a unique identifier for an item within the dictionary. Think of it as the “word” you look up in a real-world dictionary, or our “magic word” in the hashing analogy.
- Keys in a Python dictionary must be unique; you cannot have two identical keys in the same dictionary.
- Keys must be of an immutable type. This means you can use strings, numbers (integers or floats), or tuples as keys, but you cannot use mutable types like lists or other dictionaries as keys. (This immutability requirement is related to how hashing works).
- Value: The value is the actual data or information associated with a particular key. Think of it as the “definition” of the word in a real dictionary, or the information stored in that magical filing cabinet drawer labeled with the key.
- Values can be of any data type (strings, numbers, lists, other dictionaries, etc.).
- Values do not have to be unique; different keys can point to the same value.
Why Dictionaries are a Perfect Fit for Word Counting:
For our word frequency counter project, a dictionary provides an ideal structure:
- Each unique word encountered in the text will serve as a key in our dictionary (e.g., the string
"hello"). Since keys must be unique, this naturally handles the requirement of counting unique words. - The number of times that specific word has appeared (its frequency or count) will be the value associated with that word-key (e.g., the integer
5).
So, an item in our dictionary might look like:"hello": 5.
Basic Dictionary Syntax in Python:
- You create a dictionary using curly braces
{}. - Empty Dictionary:
my_empty_dictionary = {} - Dictionary with Initial Items: Key-value pairs are specified with a colon
:between the key and the value, and pairs are separated by commas.
student_scores = {"Priya": 85, "Rohan": 92, "Ananya": 78}
Here, “Priya”, “Rohan”, and “Ananya” are keys, and 85, 92, 78 are their corresponding values.
Standalone Examples of Dictionary Operations (Before the Project):
Let’s look at some basic operations before we apply them to the word counter.
# Creating a dictionary
student_ages = {"Priya": 15, "Rohan": 16, "Sameer": 15}
print(f"Initial student ages: {student_ages}")
# Accessing a value using its key
priyas_age = student_ages["Priya"]
print(f"Priya's age is: {priyas_age}") # Output: Priya's age is: 15
# What if the key doesn't exist? This would cause a KeyError:
# ananyas_age = student_ages["Ananya"] # This line would raise KeyError
# Safer access using .get(key, default_value_if_not_found)
ananyas_age = student_ages.get("Ananya", "Not found in dictionary")
print(f"Ananya's age is: {ananyas_age}") # Output: Ananya's age is: Not found in dictionary
rohans_age_check = student_ages.get("Rohan", "Not found")
print(f"Rohan's age check: {rohans_age_check}") # Output: Rohan's age check: 16
# Adding a new key-value pair
student_ages["Vikram"] = 17 # Vikram is a new student
print(f"After adding Vikram: {student_ages}")
# Output: After adding Vikram: {'Priya': 15, 'Rohan': 16, 'Sameer': 15, 'Vikram': 17}
# Modifying the value for an existing key
student_ages["Priya"] = 16 # Priya had a birthday!
print(f"After Priya's birthday: {student_ages}")
# Output: After Priya's birthday: {'Priya': 16, 'Rohan': 16, 'Sameer': 15, 'Vikram': 17}
The KeyError Problem (and the .get() Solution for Counting):
As shown above, if you try to access a dictionary value using square brackets [] for a key that doesn’t exist (e.g., word_counts["new_word"] before “new_word” has been added), Python will raise a KeyError, and your program will stop. This is a common issue when you’re building up a dictionary, like in our word counter, because the first time you encounter any word, it won’t be in the dictionary yet.
We could use an if key in dictionary: check before accessing, like this:
# word_counts = {}
# word = "example"
# if word in word_counts:
# word_counts[word] = word_counts[word] + 1
# else:
# word_counts[word] = 1
This works perfectly fine. However, Python dictionaries have a more convenient and often more readable method for this specific pattern of “get the value if it exists, or use a default if it doesn’t”: the .get(key, default_value) method.
my_dictionary.get(the_key, default_if_key_not_found)- It looks for
the_keyinmy_dictionary. - If
the_keyis found, it returns the corresponding value. - If
the_keyis not found, it returns thedefault_if_key_not_foundthat you provided. If you don’t provide a default, it returnsNone.
- It looks for
For our word counter, we want to get the current count of a word. If the word is new (not in the dictionary yet), its count should be considered 0. So, we can use .get(word, 0).
The pattern for incrementing a count then becomes very concise:
word_counts[word] = word_counts.get(word, 0) + 1
Let’s break this down:
word_counts.get(word, 0):- Python looks for
wordas a key in theword_countsdictionary. - If
wordis found (meaning we’ve seen this word before), this part returns its current count (e.g., ifwordis “the” and it has appeared 5 times, this returns5). - If
wordis not found (it’s the first time we’re seeing this word), this part returns the default value we specified, which is0.
- Python looks for
... + 1:- We take the result from step 1 (either the existing count or
0for a new word) and add1to it.
- We take the result from step 1 (either the existing count or
word_counts[word] = ...:- We then assign this newly calculated count back to the
word_countsdictionary, associated with thewordas the key. Ifwordwas new, this step adds it to the dictionary. Ifwordalready existed, this step updates its count.
- We then assign this newly calculated count back to the
This single line elegantly handles both new words and existing words!
Looping Through a Dictionary to See the Results:
Once we have our word_counts dictionary populated, we’ll want to display all the words and their frequencies. The .items() method of a dictionary is perfect for this. It returns a special view object that provides key-value pairs, which we can easily loop through.
# Example:
word_frequencies = {"python": 5, "is": 7, "fun": 3, "learning": 4, "python": 6}
# Oh, wait, a key can only appear once! "python" would have been updated.
# Let's assume it's actually:
word_frequencies = {"python": 6, "is": 7, "fun": 3, "learning": 4}
print("\n--- Word Frequencies Report ---")
# The .items() method gives us each key and its corresponding value together.
# We can unpack them into two loop variables, e.g., 'current_word' and 'current_count'.
for current_word, current_count in word_frequencies.items():
print(f"The word '{current_word}' appeared {current_count} times.")
print("----------------------------")
Output (Note: In Python 3.7+ dictionaries remember insertion order. In older versions, the print order might vary):
--- Word Frequencies Report ---
The word 'python' appeared 6 times.
The word 'is' appeared 7 times.
The word 'fun' appeared 3 times.
The word 'learning' appeared 4 times.
----------------------------
Now we have the core ideas for our word frequency counter!
Building the word_frequency.py Script
Let’s structure the code for our word_frequency.py. We’ll create a main function to handle the text processing and then a block to run it.
1. Define the analyze_text_frequency(text_input_string) function:
This function will do the main work: take the input text, clean it, count word frequencies, and return the dictionary of counts.
def analyze_text_frequency(text_input_string: str) -> dict:
"""
Analyzes a string of text to count the frequency of each word.
Converts text to lowercase and removes basic punctuation before counting.
Args:
text_input_string (str): The text to analyze.
Returns:
dict: A dictionary where keys are unique words and values are their counts.
"""
if not isinstance(text_input_string, str):
print("Error: Input must be a string.")
return {} # Return an empty dictionary for invalid input type
print(f"\nStarting analysis for text snippet: '{text_input_string[:60]}...'")
# Step 1: Normalize the text - Convert to lowercase.
# This is crucial so that "The", "the", and "THE" are all counted as the same word "the".
normalized_text = text_input_string.lower()
# print(f" Normalized (lowercase) snippet: '{normalized_text[:60]}...'") # Optional debug
# Step 2: Basic Punctuation Removal.
# For this educational example, we'll replace a common set of punctuation marks with spaces.
# This helps to separate words correctly (e.g., "end." becomes "end ").
# A more advanced approach might use regular expressions or Python's string.punctuation.
punctuation_to_remove = ['.', ',', '!', '?', ';', ':', '"', '(', ')', '\n', '\t', "'s"]
for punc_char in punctuation_to_remove:
normalized_text = normalized_text.replace(punc_char, " ")
# After replacing punctuation with spaces, there might be multiple spaces together.
# The .split() method later will handle these multiple spaces correctly.
# print(f" Text after punctuation cleanup snippet: '{normalized_text[:60]}...'") # Optional debug
# Step 3: Split the cleaned text into a list of individual words.
# The .split() string method (with no arguments) splits the string wherever it finds
# whitespace (one or more spaces, tabs, newlines) and returns a list of the words.
words_list = normalized_text.split()
if not words_list:
print(" No words found in the text after cleaning.")
return {} # Return an empty dictionary if no words are left
# print(f" First 10 words after splitting: {words_list[:10]}") # Optional debug
# Step 4: Initialize an empty dictionary to store the word frequencies.
word_frequencies_dict = {}
# Step 5: Loop through the 'words_list'. For each word, update its count.
for current_word in words_list:
# This is the core counting pattern using .get():
# 1. word_frequencies_dict.get(current_word, 0):
# - Tries to get the current count of 'current_word' from the dictionary.
# - If 'current_word' is already a key, its current count is returned.
# - If 'current_word' is NOT yet a key (first time seeing this word),
# the default value '0' is returned.
# 2. ... + 1:
# - We add 1 to the value obtained in step 1 (either the existing count or 0).
# 3. word_frequencies_dict[current_word] = ...:
# - This new count is then assigned as the value for 'current_word' in the dictionary.
# - If 'current_word' was new, it's added as a new key-value pair.
# - If 'current_word' existed, its value (count) is updated.
word_frequencies_dict[current_word] = word_frequencies_dict.get(current_word, 0) + 1
print(f"\n Analysis complete: Processed {len(words_list)} words in total.")
print(f" Found {len(word_frequencies_dict)} unique words.")
# Step 6: Return the dictionary containing the word counts.
return word_frequencies_dict
# 2. Main execution block (this code runs when you execute `python word_frequency.py`):
if __name__ == "__main__":
# You can use a predefined multiline string for easy testing:
sample_text_for_analysis = """
This is a sample text. This sample text is purely for the purpose of testing
our word frequency counter program. Learning Python is fun, and Python is also
a very versatile programming language. Count every word, yes every single word!
Is this fun or is this challenging? Python Python Python!
"""
# Alternatively, to make it interactive, you could get input from the user:
# sample_text_for_analysis = input("Please enter the text you would like to analyze:\n")
print("--- Starting Word Frequency Analysis Application ---")
if not sample_text_for_analysis.strip(): # Check if input is not just empty
print("No text provided for analysis. Exiting.")
else:
# Call our main analysis function
frequency_dictionary = analyze_text_frequency(sample_text_for_analysis)
# Display the results from the returned dictionary
print("\n========== Word Frequency Results ==========")
if not frequency_dictionary: # Check if the dictionary is empty
print("The analysis resulted in no word frequencies to display (e.g., text was empty or only punctuation).")
else:
# .items() gives us key-value pairs (word, count) to loop through.
# We can sort these items for a more organized display, for example, by word alphabetically.
# sorted(dictionary.items()) sorts by key by default.
for word_item, count_item in sorted(frequency_dictionary.items()):
print(f"The word '{word_item}' appeared {count_item} time(s).")
print("==========================================")
print("\n--- Word Frequency Analysis Application Finished ---")
This structured approach, with a dedicated function for the core logic, makes the script cleaner and the analyze_text_frequency function potentially reusable if you wanted to analyze text from different sources.
Post-Project Deep Dive: More Dictionary Methods and Common Use Cases
Now that you’ve built a project using dictionaries, let’s explore a few more useful dictionary methods and reiterate common scenarios where dictionaries are particularly powerful.
-
my_dict.keys():- Purpose: Returns a special “view object” that displays a list-like collection of all the keys currently in the dictionary.
- Usage: You can iterate over this view directly in a
forloop if you only need the keys, or you can convert it to an actuallistusinglist(my_dict.keys())if you need list-specific methods. - Example:
student_scores = {"Priya": 92, "Rohan": 88, "Ananya": 95, "Vikram": 78} all_student_names = student_scores.keys() print(f"Object returned by .keys(): {all_student_names}") # Output: Object returned by .keys(): dict_keys(['Priya', 'Rohan', 'Ananya', 'Vikram']) print(f"Keys as a list: {list(all_student_names)}") # Output: Keys as a list: ['Priya', 'Rohan', 'Ananya', 'Vikram'] print("\nStudent Names:") for name in student_scores.keys(): # Looping through the keys print(name)
-
my_dict.values():- Purpose: Returns a view object of all the values in the dictionary.
- Usage: Similar to
.keys(), you can iterate over it or convert it to alist. - Example:
student_scores = {"Priya": 92, "Rohan": 88, "Ananya": 95, "Vikram": 78} all_the_scores = student_scores.values() print(f"Object returned by .values(): {all_the_scores}") # Output: Object returned by .values(): dict_values([92, 88, 95, 78]) print(f"Values as a list: {list(all_the_scores)}") # Output: Values as a list: [92, 88, 95, 78] total_score = 0 for score_value in student_scores.values(): # Looping through the values total_score += score_value average_score = total_score / len(student_scores) if student_scores else 0 print(f"\nAverage score: {average_score:.2f}") # .2f formats float to 2 decimal places
-
del my_dict[key]:- Purpose: This is a Python statement (not a method) used to delete the key-value pair associated with the given
keyfrom the dictionary. - Behavior: If the
keyis not found in the dictionary,delwill raise aKeyError. - Example:
fruit_colors = {"apple": "red", "banana": "yellow", "grape": "purple"} print(f"Original fruit colors: {fruit_colors}") del fruit_colors["banana"] # Remove the entry for "banana" print(f"After deleting banana: {fruit_colors}") # Output: After deleting banana: {'apple': 'red', 'grape': 'purple'} # This would cause a KeyError: # del fruit_colors["mango"]
- Purpose: This is a Python statement (not a method) used to delete the key-value pair associated with the given
-
removed_value = my_dict.pop(key, default_value_if_not_found):- Purpose: This method removes the item with the specified
keyfrom the dictionary and, importantly, returns its correspondingvalue. - Behavior:
- If the
keyis found, the key-value pair is removed, and the associated value is returned. - If the
keyis not found AND adefault_value_if_not_foundis provided as the second argument,pop()will return thatdefault_value(and the dictionary remains unchanged, no error is raised). - If the
keyis not found and nodefault_value_if_not_foundis provided,pop()will raise aKeyError.
- If the
- Example:
employee_salaries = {"Aarav": 50000, "Meera": 60000, "Dev": 55000} print(f"Initial salaries: {employee_salaries}") meeras_salary = employee_salaries.pop("Meera") # Meera is leaving print(f"Meera's salary was {meeras_salary}. She has been removed.") print(f"Remaining salaries: {employee_salaries}") # Output: # Meera's salary was 60000. She has been removed. # Remaining salaries: {'Aarav': 50000, 'Dev': 55000} # Trying to pop a non-existent key, but providing a default value: kavyas_salary = employee_salaries.pop("Kavya", "Employee not found") print(f"Kavya's salary status: {kavyas_salary}") # Output: Kavya's salary status: Employee not found print(f"Salaries remain: {employee_salaries}") # Output: Salaries remain: {'Aarav': 50000, 'Dev': 55000}
- Purpose: This method removes the item with the specified
Common Use Cases for Dictionaries (Reiteration and Expansion):
- Storing Configurations or Settings: Dictionaries are excellent for managing various settings for an application, where each setting has a name (key) and a value.
app_config = {"theme": "dark", "font_size": 12, "notifications_enabled": True} - Representing Objects or Records with Labeled Fields: When you have an entity (like a student, a book, a product) that has several distinct properties, a dictionary is a natural way to store this structured information. Each piece of information (like name, ID, price) is clearly labeled by its key.
book_details = { "title": "Python for Beginners", "author": "A. Coder", "year_published": 2023, "chapters": ["Introduction", "Variables", "Functions", "Lists"], "publisher_info": { # Dictionaries can contain other dictionaries (nested structure) "name": "LearnWell Publishers", "city": "Tech City" } } print(f"The book '{book_details['title']}' has {len(book_details['chapters'])} chapters.") print(f"Published by: {book_details['publisher_info']['name']}") - Fast Lookups by a Unique Identifier: As demonstrated by the hashing concept and our word frequency counter, if you need to quickly find or access a value based on a unique identifier (the key), dictionaries are much more efficient than searching through a list, especially for large amounts of data.
- Counting and Grouping Items: Our word frequency project is a prime example. You can also use dictionaries to group items from a list based on some criteria (e.g., grouping students by the first letter of their name).
- Implementing Simple Switch/Case Logic: While Python doesn’t have a traditional
switchorcasestatement like some other languages, you can sometimes use a dictionary to map input values to functions or results, providing a clean alternative to longif/elif/elsechains for certain scenarios.
Dictionaries are truly one of Python’s “superpowers” for data organization and retrieval!
Project 2: Unique Class Enrollment Checker (Focus on Sets)
Our second project for this lecture will introduce us to another of Python’s built-in data structures: sets. Sets are particularly useful when the uniqueness of items and operations based on group membership are important.
Problem Statement:
Imagine a school has several popular after-school clubs. Let’s consider two for now: the Coding Club and the Art Club. We have lists of student names who signed up for each club. These sign-up lists might be a bit messy:
- A student might have accidentally signed up twice for the same club.
- Some enthusiastic students might be members of both clubs!
We want to write a program to help the school administration answer questions like:
- What are the unique names of all students in the Coding Club? (i.e., if “Priya” signed up twice, she should only be listed once).
- What are the unique names of all students in the Art Club?
- Which students are members of both the Coding Club AND the Art Club?
- What is the complete list of all unique students who are in either the Coding Club OR the Art Club (or perhaps both)?
- Which students are in the Coding Club but not in the Art Club?
- Which students are in the Art Club but not in the Coding Club?
Trying to answer these questions efficiently using only lists would involve a lot of looping, checking for duplicates, and complex comparisons. Python sets are designed to make these kinds of operations very simple and computationally efficient.
Introducing Sets for this Task
A set in Python is a collection data type that is:
- Unordered: Unlike lists, items in a set do not have a specific order that is preserved or guaranteed. When you print a set, the items might appear in a different order each time, or in an order you don’t expect. Because they are unordered, you cannot access items in a set using a numerical index (like
my_set[0]– this will cause an error). - Unique Items: This is a defining characteristic of sets! A set automatically ensures that it only contains one instance of each item. If you try to add an item to a set that is already present in the set, the set simply ignores the duplicate; it doesn’t raise an error, and the set’s content does not change.
- Mutable: Sets themselves are mutable, which means you can add new items to them or remove items from them after they are created.
- Items Must Be Immutable: The individual items within a set must be of an immutable type. This means you can put numbers (integers, floats), strings, or tuples inside a set. However, you cannot put mutable items like lists or other sets directly inside a set. (This requirement is related to the hashing mechanism mentioned earlier – for an item to be stored in a set, Python needs to be able to calculate a consistent hash value for it, which isn’t possible for mutable objects that can change).
Why Sets are a Perfect Fit for Our Enrollment Problem (and the Hashing Connection):
- Automatic Uniqueness: When we create a set from a list of student names (which might have duplicates from a messy sign-up sheet), the set will automatically store only the unique names. This directly solves part of our problem (e.g., finding the unique members of the Coding Club).
- Powerful Mathematical Set Operations: Python sets have built-in methods that directly correspond to common mathematical set operations like union (all items from both sets combined), intersection (items that are common to both sets), and difference (items that are in one set but not in the other). These operations are exactly what we need to answer questions like “who is in both clubs?” or “who is only in the Art club?”
- Very Fast Membership Testing (
inkeyword): Checking if an item is present in a set (e.g.,if "Priya" in art_club_members_set:) is extremely fast, especially for large sets. This is, once again, thanks to the underlying hashing mechanism. It’s much faster than checking for membership in a large list (which might involve checking each item sequentially).
Basic Set Syntax in Python:
- Creating a Set with Initial Items: You can create a set by placing items inside curly braces
{}, separated by commas. This looks similar to dictionary syntax, but for sets, you only have values, notkey:valuepairs.# Python automatically handles duplicates during creation prime_numbers_set = {2, 3, 5, 7, 11, 13, 3, 5, 2} print(f"Set of prime numbers: {prime_numbers_set}") # Output might be: {2, 3, 5, 7, 11, 13} (order is not guaranteed and may vary) student_names_in_a_group_set = {"Aarav", "Priya", "Rohan", "Priya", "Aarav"} print(f"Unique students in group: {student_names_in_a_group_set}") # Output example: Unique students in group: {'Rohan', 'Priya', 'Aarav'} (order may vary) - Creating an Empty Set (Important Distinction!):
If you use just empty curly bracesmy_var = {}, Python creates an empty dictionary, not an empty set! To create an empty set, you must use theset()function without any arguments:my_empty_dictionary = {} my_empty_set = set() # This is the correct and only way to create an empty set. print(f"Type of my_empty_dictionary: {type(my_empty_dictionary)}") # Output: <class 'dict'> print(f"Type of my_empty_set: {type(my_empty_set)}") # Output: <class 'set'> - Creating a Set from a List (Very Common and Useful!):
A very frequent use case is to create a set from an existing list, especially if you want to quickly get all the unique items from that list.guest_list_with_possible_duplicates = ["Priya", "Rohan", "Ananya", "Priya", "Vikram", "Rohan", "Rohan"] unique_guests_attending = set(guest_list_with_possible_duplicates) print(f"Original guest list: {guest_list_with_possible_duplicates}") print(f"Set of unique guests attending: {unique_guests_attending}") # Example Output: Set of unique guests attending: {'Ananya', 'Priya', 'Rohan', 'Vikram'} (order may vary)
Adding Items to a Set:
- You use the
.add(item)method to add a single item to a set. - If the item you’re trying to add is already present in the set,
.add()simply does nothing (it doesn’t raise an error, and the set remains unchanged because sets only store unique items).active_club_members = {"Rohan", "Ananya"} print(f"Initial club members: {active_club_members}") active_club_members.add("Kavya") # Kavya joins print(f"After Kavya joined: {active_club_members}") # Output example: After Kavya joined: {'Kavya', 'Rohan', 'Ananya'} active_club_members.add("Rohan") # Rohan tries to join again print(f"After Rohan tried to join again: {active_club_members}") # Output example: After Rohan tried to join again: {'Kavya', 'Rohan', 'Ananya'} (set is unchanged)
Membership Testing with Sets (using in):
Checking if an item is present in a set using the in keyword is very efficient (fast!).
fruits_in_stock_set = {"apple", "banana", "orange", "mango"}
if "apple" in fruits_in_stock_set:
print("Yes, we have apples in stock!")
else:
print("Sorry, no apples today.")
if "grape" not in fruits_in_stock_set: # 'not in' also works as expected
print("We definitely don't have grapes in stock right now.")
Now that we have a basic understanding of sets, let’s see how they can solve our class enrollment problem.
Building the class_enrollment.py Script (Conceptual Walkthrough)
Let’s outline the Python code for our class_enrollment.py script. This script will use sets to manage and compare student enrollments in two different clubs.
# class_enrollment.py
# Step 1: Define the initial lists of student names for each club.
# These lists might contain duplicate names, simulating raw sign-up sheets
# or data coming from different sources.
coding_club_signups_list = ["Aarav", "Priya", "Rohan", "Aarav", "Dev", "Kavya", "Priya", "Sameer", "Rohan"]
math_club_signups_list = ["Priya", "Vikram", "Ananya", "Rohan", "Isha", "Vikram", "Dev", "Meera"]
print("--- Initial Club Signup Lists (May Contain Duplicates) ---")
print(f"Coding Club Signups (Raw List): {coding_club_signups_list}")
print(f"Math Club Signups (Raw List): {math_club_signups_list}")
# Step 2: Convert these lists into sets.
# This is a key step! The set() constructor will automatically process the lists
# and store only the unique student names for each club.
print("\n--- Unique Members in Each Club (Converted to Sets) ---")
set_coding_club_members = set(coding_club_signups_list)
set_math_club_members = set(math_club_signups_list)
print(f"Unique Coding Club Members (Set): {set_coding_club_members}")
# Example output (order of names may vary each time you run it, as sets are unordered):
# Unique Coding Club Members (Set): {'Kavya', 'Sameer', 'Aarav', 'Rohan', 'Dev', 'Priya'}
print(f"Unique Math Club Members (Set): {set_math_club_members}")
# Example output:
# Unique Math Club Members (Set): {'Ananya', 'Vikram', 'Rohan', 'Isha', 'Meera', 'Dev', 'Priya'}
# Now, let's answer the specific questions using set operations:
# Question 3: Which students are members of BOTH the Coding Club AND the Math Club?
# This is the "intersection" of the two sets. It finds items that are common to both.
print("\n--- Enrollment Analysis Results ---")
students_in_both_clubs = set_coding_club_members.intersection(set_math_club_members)
# An alternative way to write intersection is using the '&' operator:
# students_in_both_clubs = set_coding_club_members & set_math_club_members
print(f"Students enrolled in BOTH Coding AND Math Club: {students_in_both_clubs}")
# Example output: Students enrolled in BOTH Coding AND Math Club: {'Priya', 'Rohan', 'Dev'}
# Question 4: What is the complete list of all unique students in EITHER club (or both)?
# This is the "union" of the two sets. It combines all items from both sets,
# and since it's a set, duplicates between the original sets are automatically handled (each student appears once).
all_students_in_either_club = set_coding_club_members.union(set_math_club_members)
# Alternative using the '|' operator:
# all_students_in_either_club = set_coding_club_members | set_math_club_members
print(f"All unique students enrolled in EITHER Coding OR Math Club (or both): {all_students_in_either_club}")
# Example output (order may vary):
# All unique students in EITHER club: {'Ananya', 'Kavya', 'Aarav', 'Rohan', 'Dev', 'Vikram', 'Isha', 'Sameer', 'Meera', 'Priya'}
# Question 5: Which students are in the Coding Club BUT NOT in the Math Club?
# This is the "difference" between two sets. The order matters here.
# set_A.difference(set_B) gives items that are in set A but not in set B.
students_only_in_coding_club = set_coding_club_members.difference(set_math_club_members)
# Alternative using the '-' operator:
# students_only_in_coding_club = set_coding_club_members - set_math_club_members
print(f"Students ONLY in Coding Club (and not in Math Club): {students_only_in_coding_club}")
# Example output: Students ONLY in Coding Club (and not in Math Club): {'Kavya', 'Aarav', 'Sameer'}
# Question 6: Which students are in the Math Club BUT NOT in the Coding Club?
students_only_in_math_club = set_math_club_members.difference(set_coding_club_members)
# Alternative using the '-' operator:
# students_only_in_math_club = set_math_club_members - set_coding_club_members
print(f"Students ONLY in Math Club (and not in Coding Club): {students_only_in_math_club}")
# Example output: Students ONLY in Math Club (and not in Coding Club): {'Ananya', 'Vikram', 'Isha', 'Meera'}
# (Optional Bonus) Question 7: Which students are in one club OR the other, but NOT in both?
# This is called the "symmetric difference". It finds all items that are in exactly one of the sets.
students_in_one_club_but_not_both = set_coding_club_members.symmetric_difference(set_math_club_members)
# Alternative using the '^' (caret) operator:
# students_in_one_club_but_not_both = set_coding_club_members ^ set_math_club_members
print(f"Students in EITHER Coding or Math club, but NOT in BOTH: {students_in_one_club_but_not_both}")
# Example output: Students in EITHER Coding or Math club, but NOT in BOTH: {'Kavya', 'Ananya', 'Sameer', 'Dev', 'Vikram', 'Isha', 'Meera', 'Aarav'}
# (Wait, checking this output... Aarav, Sameer, Kavya are only in Coding. Ananya, Vikram, Isha, Meera are only in Math. Dev is in both. Oh, the example output for symmetric difference in the plan was off.
# Corrected example: If Coding = {A, P, R, D, K, S} and Math = {P, V, N, R, I, D, M}
# Both = {P, R, D}
# Symmetric diff = (Coding U Math) - (Coding intersect Math)
# Symmetric diff = {A, K, S, V, N, I, M}
# My example lists had:
# Coding: Aarav, Priya, Rohan, Dev, Kavya, Sameer
# Math: Priya, Vikram, Ananya, Rohan, Isha, Dev, Meera
# Both: Priya, Rohan, Dev
# Only Coding: Aarav, Kavya, Sameer
# Only Math: Vikram, Ananya, Isha, Meera
# Symmetric Difference should be: Aarav, Kavya, Sameer, Vikram, Ananya, Isha, Meera.
# Let's re-verify the symmetric difference based on the example lists:
# set_coding_club_members = {'Aarav', 'Priya', 'Rohan', 'Dev', 'Kavya', 'Sameer'}
# set_math_club_members = {'Priya', 'Vikram', 'Ananya', 'Rohan', 'Isha', 'Dev', 'Meera'}
# Expected symmetric_difference: {'Aarav', 'Kavya', 'Sameer', 'Vikram', 'Ananya', 'Isha', 'Meera'}
# This is standard boilerplate for scripts.
if __name__ == "__main__":
print("\n--- Class Enrollment Analysis Script Complete ---")
# The print statements above already show the results for this demonstration script.
# In a more complex application, you might have functions that perform these operations
# and return the resulting sets, and then this __main__ block would call those functions
# and perhaps print the results in a more formatted way.
This script clearly demonstrates how easily sets handle tasks of finding unique items and performing logical comparisons (like finding common members or differences) between collections.
Post-Project Deep Dive: More Set Methods and Common Use Cases
Beyond the core set operations (union, intersection, difference, symmetric_difference), sets have several other handy methods:
-
my_set.discard(item_to_remove):- Purpose: Removes
item_to_removefrommy_setif it is present. - Key Behavior: If
item_to_removeis not in the set,discard()does nothing – it does not raise an error. This is often safer than themy_set.remove(item)method (which would raise aKeyErrorif the item isn’t found, similar to dictionaries). - Example:
active_players_set = {"Rohan", "Priya", "Dev", "Aarav"} print(f"Active players: {active_players_set}") active_players_set.discard("Priya") # Priya is removed print(f"After discarding Priya: {active_players_set}") active_players_set.discard("Vikram") # Vikram is not in the set; nothing happens, no error. print(f"After trying to discard Vikram (who wasn't there): {active_players_set}")
- Purpose: Removes
-
removed_item = my_set.pop():- Purpose: Removes and returns an arbitrary (random-seeming) item from the set.
- Key Behavior: Since sets are unordered, you cannot predict which item will be removed and returned by
pop(). If the set is empty, callingpop()will raise aKeyError. - Example:
(Each run might pop different fruits first).available_fruits_set = {"apple", "banana", "cherry", "mango"} print(f"Fruits set: {available_fruits_set}") if available_fruits_set: # Check if the set is not empty before popping a_fruit = available_fruits_set.pop() print(f"Popped a fruit: {a_fruit}") print(f"Remaining fruits in set: {available_fruits_set}") another_fruit = available_fruits_set.pop() print(f"Popped another fruit: {another_fruit}") print(f"Remaining fruits in set: {available_fruits_set}")
-
my_set.update(another_iterable)(ormy_set |= another_iterable):- Purpose: Adds all items from
another_iterable(which can be another set, a list, a tuple, or any other iterable collection) intomy_set. - Key Behavior: This modifies
my_setin-place. Duplicate items (those already present inmy_setor duplicates withinanother_iterable) are automatically ignored due to the nature of sets. - Example:
set_alpha = {1, 2, 3} set_beta = {3, 4, 5} # '3' is common, '4', '5' are new list_gamma = [5, 6, 1, 7] # '5', '1' are already present or will be, '6', '7' are new print(f"Original set_alpha: {set_alpha}") set_alpha.update(set_beta) # Add all items from set_beta into set_alpha print(f"set_alpha after update with set_beta: {set_alpha}") # Output: set_alpha after update with set_beta: {1, 2, 3, 4, 5} set_alpha.update(list_gamma) # Add all items from list_gamma into set_alpha print(f"set_alpha after update with list_gamma: {set_alpha}") # Output: set_alpha after update with list_gamma: {1, 2, 3, 4, 5, 6, 7}
- Purpose: Adds all items from
-
my_set.clear():- Purpose: Removes all items from
my_set, leaving it as an empty set. - Example:
numbers_to_clear = {10, 20, 30, 40, 50} print(f"Original numbers_to_clear: {numbers_to_clear}") numbers_to_clear.clear() print(f"numbers_to_clear after .clear(): {numbers_to_clear}") # Output: numbers_to_clear after .clear(): set()
- Purpose: Removes all items from
Common and Powerful Use Cases for Sets:
- Removing Duplicates from a List (A Very Common Python Idiom!): If you have a list and want to get a new list containing only the unique items from the original, the most Pythonic and efficient way is to convert the list to a set, and then back to a list.
my_list_with_many_duplicates = ["apple", "banana", "apple", "orange", "banana", "banana", "grape"] print(f"Original list: {my_list_with_many_duplicates}") # Convert to a set to remove duplicates, then convert back to a list unique_items_as_list = list(set(my_list_with_many_duplicates)) print(f"List with unique items: {unique_items_as_list}") # Example Output: List with unique items: ['orange', 'grape', 'apple', 'banana'] # (Note: the order of items in the final list is not guaranteed to be the same as their first appearance in the original list because sets are unordered.) - Efficient Membership Testing: As emphasized before, checking if an item exists in a collection using the
inkeyword (e.g.,if user_input in valid_commands_set:) is significantly faster for sets than for lists, especially when the collection (set or list) is large. This is due to the hashing mechanism. So, if your program frequently needs to check “is this item part of my group of allowed items?”, using a set for the “group of allowed items” is often a performance booster. - Performing Set Mathematics: Whenever your problem involves finding commonalities (intersection), complete combinations (union), or differences between groups of items, Python sets provide direct, readable, and efficient tools for these operations, as demonstrated in our class enrollment project.
Summary: Dictionaries vs. Sets vs. Lists
Let’s consolidate our understanding by clearly comparing these three important Python collection data structures:
| Feature | List ([]) |
Dictionary ({}) |
Set (set() or {items}) |
|---|---|---|---|
| Ordered? | Yes, items maintain their insertion order. This means the order in which you add items is the order in which they are stored and typically retrieved. | Python 3.7+: Yes (insertion order is preserved). Before Python 3.7: Effectively No (unordered). However, the primary characteristic of a dictionary is its key-based access, not its sequence order. |
No, items are generally considered unordered. The order of items in a set can change and should not be relied upon for any logic. |
| Indexed by number? | Yes (e.g., my_list[0], my_list[-1]). You access items using their numerical position (starting from 0). |
No. Items are accessed by their unique keys (e.g., my_dict['user_name']), not by a numerical position. |
No. Sets do not support numerical indexing because they are unordered. You cannot do my_set[0]. |
| Mutable? | Yes. You can change, add, or remove items from a list after it has been created. | Yes. You can add new key-value pairs, change the value associated with a key, or remove key-value pairs. | Yes. You can add new items to a set or remove items from it. (However, the items within a set must be immutable). |
| Duplicates allowed? | Yes. A list can contain multiple identical items (e.g., [1, 2, 2, 3]). |
No for Keys (Keys within a dictionary must be unique). Yes for Values (Different keys can certainly have the same value). |
No. Sets automatically store only unique items. If you try to add an item that’s already there, the set remains unchanged. |
| Stores? | Individual items in an ordered sequence. | Key-Value pairs. Each item in a dictionary consists of a key and its associated value. | Individual unique items. |
| Primary Use Cases | Use when the order of items is important, you need to access items by their numerical position (index), or when you explicitly need to store duplicate items. (e.g., steps in a recipe, a history of user actions, a simple to-do list where order might matter). | Use when you need to associate related pieces of data (like a word and its definition, or a student’s name and their ID). Dictionaries are essential for fast lookups when you have a unique identifier (the key) for your data. Also great for representing structured records or objects. (e.g., phone book, configuration settings, word frequency counts). | Use when you need to ensure all items in a collection are unique, for very fast checks of whether an item exists in the collection (membership testing using in), or when you need to perform mathematical set operations like finding common items (intersection), combining all unique items (union), or finding differences between collections. (e.g., finding unique visitors to a website, checking for common elements between two groups, removing duplicates from a list). |
| Creation Example | planets = ["Mercury", "Venus", "Earth"] |
capitals = {"India": "New Delhi", "Japan": "Tokyo"} |
unique_colors = {"red", "green", "blue", "red"} empty_s = set() |
Choosing the right data structure is a key skill in programming. It can significantly impact the clarity of your code, its efficiency (how fast it runs), and how easy it is to manage and modify. As you encounter more programming problems, you’ll develop a better intuition for when a list, a dictionary, or a set (or perhaps another data structure) is the most appropriate tool for the job.
Concepts Learned Recap
This lecture introduced two new powerful data structures and related concepts, building on your existing Python knowledge:
- Hashing (Conceptual Introduction): We gained a basic understanding of hashing as an underlying technique that enables dictionaries and sets to perform certain operations (like lookups, additions, and deletions) very quickly, especially with large amounts of data.
- Dictionaries:
- We learned that dictionaries are collections that store data as key-value pairs.
- We covered the syntax for creating dictionaries (e.g.,
my_scores = {"Rohan": 95}or using{}for an empty one), adding new key-value pairs (my_dict[new_key] = new_value), and retrieving values using their keys (my_dict[existing_key]). - We mastered the use of
my_dict.get(key, default_value)as a safe and convenient way to access dictionary entries, especially useful for providing a default (like0when counting) if a key doesn’t exist yet. This was central to our word frequency counter. - We practiced looping through dictionary keys, values, and key-value pairs (items) using
my_dict.keys(),my_dict.values(), andmy_dict.items(). - We also looked at other useful dictionary methods like
del my_dict[key]for deleting entries andmy_dict.pop(key, default_value)for removing an entry and getting its value. - We built a Word Frequency Counter project, which is a classic application of dictionaries.
- Sets:
- We learned that sets are unordered collections of unique items.
- We covered the syntax for creating sets (e.g.,
unique_numbers = {1, 2, 3}or usingset()for an empty set or to convert from a list) and saw how they automatically handle and discard duplicate entries. - We practiced adding items to a set using
my_set.add(item). - A key focus was on performing powerful set operations for comparing and combining sets:
- Union (
|orset1.union(set2)): To get all unique items present in either set. - Intersection (
&orset1.intersection(set2)): To find items that are common to both sets. - Difference (
-orset1.difference(set2)): To find items that are in one set but not in another. - (Optional) Symmetric Difference (
^orset1.symmetric_difference(set2)): To find items in either set, but not in both.
- Union (
- We also touched upon other useful set methods like
.discard(item)(for safe removal),.pop()(to remove an arbitrary item),.update(other_iterable)(to add multiple items), and.clear()(to empty a set). - We built a Unique Class Enrollment Checker project, which highlighted the utility of sets for managing group memberships and performing comparisons.
- Choosing Appropriate Data Structures: We started to see more clearly how the distinct characteristics and built-in operations of lists, dictionaries, and sets make them suitable for different kinds of programming problems and data management tasks.
These new tools – dictionaries and sets – along with the concept of hashing, will significantly expand the complexity, efficiency, and elegance of the Python programs you can create!
Homework Challenges
Ready to solidify your understanding and get more practice with dictionaries and sets? Here are some challenges:
-
Word Frequency Project Enhancements:
- Punctuation and Case (More Robustly): In our
word_frequency.pyproject, our punctuation removal was basic.- Can you make the case-insensitivity more robust by ensuring
text.lower()is applied before splitting into words and before words are added to the dictionary? - Research Python’s
string.punctuationconstant (import string; print(string.punctuation)). Try to modify your cleaning step to remove all punctuation marks found in this constant from your text string before splitting into words.
- Can you make the case-insensitivity more robust by ensuring
- Ignore Common “Stop Words”: Many text analyses ignore very common words (like “the”, “a”, “is”, “in”, “and”, “to”, “of”, “it”, “you”, “he”, “she”, etc.) as they don’t usually add much specific meaning to frequency counts. Create a
setof these “stop words.” Modify your word frequency counter to skip counting any word that is present in your set of stop words. - Find the N Most Frequent Words: After generating the
word_countsdictionary, can you write code to find and print, for example, the top 5 or top 10 most frequent words and their counts?- Hint: This is a bit more advanced. Dictionaries themselves aren’t directly sortable in a way that’s easy to get the “top N by value.” You’ll likely want to:
- Get the key-value pairs using
word_counts.items(). This gives you a list-like view of tuples, e.g.,[('the', 50), ('python', 10)]. - Use the built-in
sorted()function to sort this list of tuples. You’ll need to tellsorted()to sort based on the count (which is the second element, index 1, of each tuple) and in descending order (so the highest count comes first). Thekeyargument ofsorted()is used for this, often with a small anonymous function called alambdafunction:sorted_items = sorted(word_counts.items(), key=lambda item_tuple: item_tuple[1], reverse=True). - Once this list of tuples is sorted by frequency, you can easily get the top N items by slicing the list (e.g.,
sorted_items[:5]for the top 5).
- Get the key-value pairs using
- Hint: This is a bit more advanced. Dictionaries themselves aren’t directly sortable in a way that’s easy to get the “top N by value.” You’ll likely want to:
- Punctuation and Case (More Robustly): In our
-
Class Enrollment Project Enhancements:
- Interactive Input for Rosters: Modify the
class_enrollment.pyscript. Instead of having predefined lists of students, prompt the user to enter the names for the Coding Club (perhaps as a comma-separated string, which you can then.split(',')and.strip()each name) and then do the same for the Art Club. - Add/Remove Students from Clubs Interactively: Add functions to your enrollment script that allow a user (perhaps through a simple menu system like our To-Do app or calculator) to:
- Add a new student name to a specific club’s roster (which should be a set).
- Remove a student name from a specific club’s roster.
- After each modification, recalculate and display the various set comparisons (intersection, union, differences).
- More Clubs & Advanced Comparisons: Add a third club (e.g., “Debate Club”) with its own set of students. Then, add functionality to find:
- Students who are in all three clubs. (Hint:
set1.intersection(set2, set3)) - Students who are in, for example, Coding Club and Math Club, but not in the Art Club.
- Students who are in all three clubs. (Hint:
- Interactive Input for Rosters: Modify the
-
New Problem: Finding Pairs that Sum to a Target (Using a Set for Efficiency)
- Problem Statement: Write a Python function that takes a list of distinct numbers (e.g.,
numbers = [10, 20, 5, 15, 30, 25, 12, 35]) and a separatetarget_sumvalue (e.g.,target = 40). The function should determine if there are any two distinct numbers within the input list that add up exactly to thetarget_sum. The function should returnTrueif such a pair exists, andFalseotherwise. - Example:
- For
numbers = [10, 20, 5, 15, 30, 25]andtarget_sum = 40, the function should returnTrue(because 10 + 30 = 40, or 15 + 25 = 40). - For
numbers = [10, 20, 5, 15]andtarget_sum = 70, it should returnFalse.
- For
- Hint (Using a Set for Efficiency):
- Initialize an empty set, let’s call it
seen_numbers. This set will store numbers from the list that you have already processed. - Loop through each
current_numin the inputnumberslist. - For each
current_num, calculate thecomplementthat you would need to find in the list to make up thetarget_sum. The complement iscomplement = target_sum - current_num. - Now, check if this calculated
complementis already present in yourseen_numbersset.- If the
complementis inseen_numbers, it means you have found a pair that sums to the target! (Thecurrent_numand thecomplementyou found in the set). So, you can immediately returnTrue.
- If the
- If the
complementis not inseen_numbers, it means you haven’t found a pair with thecurrent_numyet. So, add thecurrent_numto yourseen_numbersset (because you’ve now “seen” it, and it might serve as a complement for a future number you encounter in the list). - If you finish looping through all the numbers in the list and haven’t returned
True, it means no such pair was found. In this case, your function should returnFalseafter the loop.
- Initialize an empty set, let’s call it
- Think about it: Why is using a set for
seen_numbersmore efficient here than, say, using two nestedforloops to check every possible pair of numbers in the original list? (Consider the speed of theinoperator for sets versus searching in lists).
- Problem Statement: Write a Python function that takes a list of distinct numbers (e.g.,
These challenges are designed to push you to apply dictionaries and sets in slightly more complex and varied scenarios. Remember to break down the problems, think about the strengths of each data structure, and test your code incrementally. Good luck, and enjoy exploring these powerful Python tools!