This page shows the source code for Session-2-3-Examples.py with teaching notes, PURPOSE comments, and VS Code instructions.
import sys
sys.stdout.reconfigure(encoding='utf-8')
"""
Session 3: Data Structures - Code Examples
Introduction to Python Course
"""
# ============================================
# PART 1: Lists
# ============================================
print("=" * 50)
print("PART 1: Lists")
print("=" * 50)
# ======================================================
# WHAT THIS SECTION DOES:
# 1) PURPOSE: A list is Python's most used data structure.
# It stores multiple values in a single variable, in order,
# and you can change it at any time (add, remove, modify).
# Think of it as a shopping list or a numbered to-do list.
#
# 2) VS CODE: Run each numbered Example block separately -
# select its lines and press Shift+Enter. After each one,
# try the TRY suggestion before moving to the next example.
# ======================================================
# ------------------------------------------------------
# HOW LISTS WORK
# ------------------------------------------------------
#
# CREATE a list with square brackets, items separated by commas:
# fruits = ["apple", "banana", "cherry"]
#
# INDEXING - access one item by its position number:
# fruits[0] -> "apple" (first item, index starts at 0!)
# fruits[1] -> "banana"
# fruits[-1] -> "cherry" (last item, -1 counts from end)
# fruits[-2] -> "banana" (-2 is second from end)
#
# SLICING - get a sub-list using [start:stop]:
# fruits[0:2] -> ["apple", "banana"] (stop is NOT included)
# fruits[:2] -> ["apple", "banana"] (start defaults to 0)
# fruits[1:] -> ["banana", "cherry"] (stop defaults to end)
# fruits[::2] -> every 2nd item
# fruits[::-1]-> reversed list
#
# MODIFY - lists are MUTABLE (can be changed after creation):
# fruits[0] = "mango" replace an item
# fruits.append("kiwi") add to the end
# fruits.insert(1, "pear") add at a specific position
# fruits.remove("banana") remove by value
# fruits.pop() remove and return the last item
#
# IMPORTANT: index 0 is the FIRST item. This catches beginners
# out constantly - the first item is fruits[0], NOT fruits[1].
# ------------------------------------------------------
# Example 1: Creating lists
print("\n--- Example 1: Creating Lists ---")
# ======================================================
# WHAT: Three different list types showing that a list can
# hold strings, numbers, booleans, or even other lists.
# A list containing another list is called a nested list.
# TRY: Add your own items to the fruits list and print it.
# ======================================================
fruits = ["apple", "banana", "cherry", "date"]
numbers = [10, 20, 30, 40, 50]
mixed = [1, "hello", 3.14, True, [1, 2, 3]] # any types allowed
print(f"Fruits: {fruits}")
print(f"Numbers: {numbers}")
print(f"Mixed list: {mixed}")
# Example 2: Accessing items
print("\n--- Example 2: Accessing List Items ---")
# ======================================================
# WHAT: Shows positive indexing (0 = first) and negative
# indexing (-1 = last). Negative indexes count backwards
# from the end - very handy when you don't know the length.
# TRY: Print fruits[2] and fruits[-2] - what do you get?
# ======================================================
print(f"First fruit: fruits[0] = {fruits[0]}") # index 0 = first
print(f"Last fruit: fruits[-1] = {fruits[-1]}") # -1 = last
print(f"Second fruit: fruits[1] = {fruits[1]}") # index 1 = second
# Example 3: Slicing
print("\n--- Example 3: List Slicing ---")
# ======================================================
# WHAT: Slicing extracts a portion of a list.
# [start:stop] -> from start UP TO (not including) stop
# [:stop] -> from beginning up to stop
# [start:] -> from start to the end
# [::step] -> every step-th item
# [::-1] -> all items in reverse order
# TRY: Try numbers[2:8:2] - what pattern does it give?
# ======================================================
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(f"Original: {numbers}")
print(f"First 5 [:5]: {numbers[:5]}") # stop before index 5
print(f"Last 5 [-5:]: {numbers[-5:]}") # start 5 from end
print(f"Middle [3:7]: {numbers[3:7]}") # index 3,4,5,6
print(f"Every 2nd [::2]:{numbers[::2]}") # step=2
print(f"Reversed [::-1]:{numbers[::-1]}") # step=-1 reverses
# Example 4: Modifying lists
print("\n--- Example 4: Modifying Lists ---")
# ======================================================
# WHAT: Demonstrates the key methods for changing a list.
# append() -> adds one item to the END
# insert() -> adds one item at a chosen position
# remove() -> deletes the first item matching the value
# pop() -> removes AND returns the last item (useful
# when you need to know what was removed)
# TRY: After all modifications, print len(shopping) to see
# the final count.
# ======================================================
shopping = ["milk", "bread", "eggs"]
print(f"Original: {shopping}")
shopping.append("butter") # adds to end
print(f"After append: {shopping}")
shopping.insert(1, "cheese") # inserts at index 1
print(f"After insert: {shopping}")
shopping[0] = "almond milk" # replaces item at index 0
print(f"After change: {shopping}")
shopping.remove("bread") # removes first "bread" found
print(f"After remove: {shopping}")
last_item = shopping.pop() # removes & returns last item
print(f"Popped item: {last_item}")
print(f"Final list: {shopping}")
# Example 5: List methods
print("\n--- Example 5: List Methods ---")
# ======================================================
# WHAT: Built-in functions and methods that work on lists:
# len() -> number of items
# sum() -> total of all numbers
# max() -> largest value
# min() -> smallest value
# count() -> how many times a value appears
# index() -> position of the first match
# sorted() -> returns a NEW sorted list (original unchanged)
# .sort() -> sorts the list IN PLACE (original changed!)
# .reverse()-> reverses the list IN PLACE
# TRY: What is the difference between sorted(numbers) and
# numbers.sort()? Run both and check if the original changes.
# ======================================================
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5]
print(f"Original: {numbers}")
print(f"len(): {len(numbers)}")
print(f"sum(): {sum(numbers)}")
print(f"max(): {max(numbers)}")
print(f"min(): {min(numbers)}")
print(f"count(5): {numbers.count(5)}") # how many 5s?
print(f"index(9): {numbers.index(9)}") # where is 9?
sorted_nums = sorted(numbers) # NEW list returned
print(f"sorted() new list: {sorted_nums}")
print(f"Original unchanged: {numbers}") # still original order
numbers.sort() # modifies IN PLACE
print(f"After .sort() in place:{numbers}")
numbers.reverse() # modifies IN PLACE
print(f"After .reverse(): {numbers}")
# Example 6: List comprehensions
print("\n--- Example 6: List Comprehensions ---")
# ======================================================
# WHAT: A list comprehension is a compact way to build a
# new list from an existing sequence - all in one line.
#
# STRUCTURE:
# [expression for item in sequence]
# [expression for item in sequence if condition]
#
# The long way (for loop):
# squares = []
# for x in range(10):
# squares.append(x**2)
#
# The short way (list comprehension):
# squares = [x**2 for x in range(10)]
#
# With a filter (only include even numbers):
# evens = [x for x in range(20) if x % 2 == 0]
#
# TRY: Write a comprehension that gives the squares of
# only the even numbers from 0 to 20:
# [x**2 for x in range(20) if x % 2 == 0]
# ======================================================
squares = [x**2 for x in range(10)] # x squared for each x
print(f"Squares: {squares}")
evens = [x for x in range(20) if x % 2 == 0] # filter: only even
print(f"Evens: {evens}")
fruits = ["apple", "banana", "cherry"]
upper_fruits = [fruit.upper() for fruit in fruits] # .upper() on each
print(f"Uppercase: {upper_fruits}")
# Example 7: Nested lists
print("\n--- Example 7: Nested Lists (Matrix) ---")
# ======================================================
# WHAT: A list of lists is called a nested list (or matrix).
# Access elements with TWO indexes: [row][column].
# matrix[1][2] means: row 1 (second row), column 2 (third column).
# Row and column indexes both start at 0.
# TRY: Print matrix[2][0] - which number does that give?
# (row 2, column 0 -> should be 7)
# ======================================================
matrix = [
[1, 2, 3], # row 0
[4, 5, 6], # row 1
[7, 8, 9] # row 2
]
print(f"Matrix: {matrix}")
print(f"Row 0: {matrix[0]}") # entire first row
print(f"matrix[1][2]: {matrix[1][2]}") # row 1, column 2 -> 6
# Example 8: Practical - Grade tracker
print("\n--- Example 8: Grade Tracker ---")
# ======================================================
# WHAT: Real-world use of list math functions. :.2f inside
# the f-string formats the average to 2 decimal places.
# TRY: Add a grade of 100 to the list and recalculate.
# Does the average change as expected?
# ======================================================
grades = [85, 92, 78, 90, 88]
print(f"Grades: {grades}")
print(f"Average: {sum(grades) / len(grades):.2f}") # :.2f = 2 decimal places
print(f"Highest: {max(grades)}")
print(f"Lowest: {min(grades)}")
# ============================================
# PART 2: Tuples
# ============================================
print("\n" + "=" * 50)
print("PART 2: Tuples")
print("=" * 50)
# ======================================================
# WHAT THIS SECTION DOES:
# 1) PURPOSE: A tuple is like a list BUT it cannot be
# changed after creation (immutable). Use a tuple when
# your data should NEVER change: GPS coordinates, RGB
# colours, a person's birth date, database records.
# Tuples use () instead of [].
# Bonus: functions can return multiple values via tuples.
#
# 2) VS CODE: Run each Example block with Shift+Enter.
# The key insight to test: try modifying a tuple element
# and watch Python raise a TypeError.
# ======================================================
# ------------------------------------------------------
# HOW TUPLES WORK - LIST vs TUPLE COMPARISON
# ------------------------------------------------------
#
# LIST (mutable - can change) TUPLE (immutable - cannot change)
# fruits = ["apple", "banana"] coords = (10, 20)
# fruits[0] = "mango" <- OK coords[0] = 99 <- ERROR!
# fruits.append("kiwi") <- OK No append method on tuples
#
# SYNTAX:
# my_tuple = (1, 2, 3) standard tuple
# single = (42,) ONE-item tuple NEEDS a comma!
# single = (42) this is just the number 42, NOT a tuple
#
# UNPACKING - assign each item to its own variable in one line:
# x, y = (10, 20) x=10, y=20
# name, age, job = ("Alice", 25, "Engineer")
#
# WHY USE TUPLES?
# - Faster than lists for read-only data
# - Signals to other programmers "this data must not change"
# - Can be used as dictionary keys (lists cannot)
# - Functions use them to return multiple values at once
# ------------------------------------------------------
# Example 1: Creating tuples
print("\n--- Example 1: Creating Tuples ---")
# ======================================================
# WHAT: Shows four tuple types. The single-item tuple (42,)
# is a common gotcha - the comma is REQUIRED. Without
# the comma, Python treats (42) as just the number 42.
# TRY: Print type((42,)) and type((42)) to see the difference.
# ======================================================
coordinates = (10, 20)
person = ("Alice", 25, "Engineer")
single = (42,) # comma required for single-item tuple!
colors = ("red", "green", "blue")
print(f"Coordinates: {coordinates}")
print(f"Person: {person}")
print(f"Single item: {single} <- note the comma inside")
print(f"Colors: {colors}")
# Example 2: Accessing tuples
print("\n--- Example 2: Accessing Tuple Items ---")
# ======================================================
# WHAT: Same indexing rules as lists - index 0 is first,
# -1 is last. Reading works; writing raises a TypeError.
# ======================================================
print(f"X coordinate: coordinates[0] = {coordinates[0]}")
print(f"Y coordinate: coordinates[1] = {coordinates[1]}")
print(f"Person name: person[0] = {person[0]}")
# Example 3: Tuple unpacking
print("\n--- Example 3: Tuple Unpacking ---")
# ======================================================
# WHAT: Unpacking assigns each tuple item to a separate
# variable in one clean line. The number of variables
# on the left MUST match the number of items in the tuple.
# This is also how functions can "return" multiple values.
# TRY: Swap x and y using tuple unpacking: x, y = y, x
# Print x and y before and after to see the swap.
# ======================================================
x, y = coordinates # unpacks (10, 20) into x and y
name, age, job = person # unpacks 3-item tuple into 3 variables
print(f"x={x}, y={y}")
print(f"Name: {name}, Age: {age}, Job: {job}")
# Example 4: Tuple methods
print("\n--- Example 4: Tuple Methods ---")
# ======================================================
# WHAT: Tuples only have 2 methods (vs many for lists)
# because they cannot be modified. count() and index()
# work exactly like their list equivalents.
# ======================================================
numbers = (1, 2, 3, 2, 4, 2, 5)
print(f"Numbers: {numbers}")
print(f"len(): {len(numbers)}")
print(f"count(2): {numbers.count(2)}") # how many 2s?
print(f"index(4): {numbers.index(4)}") # where is 4?
# Example 5: Why tuples? (Immutability)
print("\n--- Example 5: Tuple Immutability ---")
# ======================================================
# WHAT: Demonstrates that tuples protect their data.
# Uncommenting point[0] = 15 will raise:
# TypeError: 'tuple' object does not support item assignment
# This is intentional - it prevents accidental changes
# to data that should stay fixed.
# TRY: Uncomment point[0] = 15, run it, read the error,
# then re-comment it.
# ======================================================
point = (10, 20)
print(f"Original point: {point}")
# point[0] = 15 # <- Uncomment to see TypeError in action!
print("Tuples cannot be modified after creation - this protects your data")
# Example 6: Returning multiple values
print("\n--- Example 6: Function Returning Tuple ---")
# ======================================================
# WHAT: A function normally returns ONE value. By returning
# a tuple, it can return TWO (or more) values at once.
# The caller unpacks the tuple into separate variables.
# This is a very common Python pattern.
# TRY: Change nums to [100, 5, 42, 73, 8] and run again.
# ======================================================
def get_min_max(numbers):
return (min(numbers), max(numbers)) # returns a 2-item tuple
nums = [3, 7, 1, 9, 4]
minimum, maximum = get_min_max(nums) # unpack returned tuple
print(f"Numbers: {nums}")
print(f"Min: {minimum}, Max: {maximum}")
# ============================================
# PART 3: Dictionaries
# ============================================
print("\n" + "=" * 50)
print("PART 3: Dictionaries")
print("=" * 50)
# ======================================================
# WHAT THIS SECTION DOES:
# 1) PURPOSE: A dictionary stores data as KEY:VALUE pairs.
# Instead of accessing items by position number (like lists),
# you access them by a meaningful name (the key).
# Think of it like a real dictionary: look up a word (key)
# to get its definition (value). Perfect for storing
# properties of one thing: a student record, a product,
# a settings file.
#
# 2) VS CODE: Run each Example with Shift+Enter. The most
# important Examples to study carefully are 2, 3, 4, and 5
# - they cover the everyday operations you will use constantly.
# ======================================================
# ------------------------------------------------------
# HOW DICTIONARIES WORK
# ------------------------------------------------------
#
# CREATE with curly braces, key:value pairs:
# student = {"name": "Alice", "age": 20, "grade": "A"}
#
# READ a value using its key in square brackets:
# student["name"] -> "Alice"
# student["age"] -> 20
#
# SAFER READ with .get() - returns None (or a default)
# instead of crashing if the key does not exist:
# student.get("email") -> None (no crash)
# student.get("email", "Unknown") -> "Unknown"
#
# WRITE / UPDATE - same syntax, just assign:
# student["email"] = "alice@school.com" add new key
# student["age"] = 21 update existing key
#
# DELETE a key:
# del student["grade"]
#
# USEFUL METHODS:
# student.keys() -> all the key names
# student.values() -> all the values
# student.items() -> all (key, value) pairs <- used in for loops
#
# CHECK if a key exists:
# "name" in student -> True
# "email" in student -> False (if not added yet)
#
# KEYS must be unique and immutable (strings or numbers).
# VALUES can be anything: strings, numbers, lists, even other dicts.
# ------------------------------------------------------
# Example 1: Creating dictionaries
print("\n--- Example 1: Creating Dictionaries ---")
# ======================================================
# WHAT: A student record stored as a dictionary. Notice the
# "courses" value is itself a list - values can be any type.
# TRY: Add a "gpa": 3.8 key:value pair and print the dict.
# ======================================================
student = {
"name": "Alice",
"age": 20,
"grade": "A",
"courses": ["Math", "Python", "Physics"] # value can be a list!
}
print(f"Student: {student}")
# Example 2: Accessing values
print("\n--- Example 2: Accessing Dictionary Values ---")
# ======================================================
# WHAT: Two ways to read a value:
# dict["key"] -> works, but CRASHES with KeyError
# if the key does not exist
# dict.get("key") -> returns None if key not found (safe)
# dict.get("key", default) -> returns default if key not found
# TRY: Try student["email"] (no .get) - you'll get a KeyError.
# Then try student.get("email") - no crash, returns None.
# ======================================================
print(f"Name: student['name'] = {student['name']}")
print(f"Age: student['age'] = {student['age']}")
print(f"Courses: student['courses'] = {student['courses']}")
print(f"Grade: student.get('grade') = {student.get('grade')}")
print(f"Email: student.get('email','Not provided')= {student.get('email', 'Not provided')}")
# Example 3: Modifying dictionaries
print("\n--- Example 3: Modifying Dictionaries ---")
# ======================================================
# WHAT: Assigning to a key adds it if new, updates it if existing.
# There is no separate "add" vs "update" method - same syntax.
# TRY: Print student before and after each change to see
# exactly which key changed.
# ======================================================
student["email"] = "alice@school.com" # new key -> adds it
student["age"] = 21 # existing key -> updates it
student["gpa"] = 3.8 # new key -> adds it
print(f"Updated student: {student}")
# Example 4: Dictionary methods
print("\n--- Example 4: Dictionary Methods ---")
# ======================================================
# WHAT: .keys(), .values(), .items() each return a view of
# the dictionary. Wrapping in list() converts the view
# to a regular list so it prints nicely.
# .items() returns (key, value) PAIRS - used heavily
# when looping (see Example 5).
# ======================================================
print(f"Keys: {list(student.keys())}")
print(f"Values: {list(student.values())}")
print(f"Items: {list(student.items())}")
if "name" in student: # check before accessing
print("'name' key exists in student")
# Example 5: Looping through dictionaries
print("\n--- Example 5: Looping Through Dictionary ---")
# ======================================================
# WHAT: Two ways to loop.
# for key in student -> gives keys only; use student[key] to get value
# for key, value in .items() -> gives BOTH at once (more readable)
# .items() unpacks each (key, value) pair directly into two variables.
# TRY: Change the loop to only print keys where the value
# is a string: if isinstance(value, str): print(...)
# ======================================================
print("Loop using keys only:")
for key in student:
print(f" {key}: {student[key]}")
print("\nLoop using .items() (key AND value together):")
for key, value in student.items(): # unpacks each pair
print(f" {key} = {value}")
# Example 6: Nested dictionaries
print("\n--- Example 6: Nested Dictionaries ---")
# ======================================================
# WHAT: Each value in class_roster is itself a dictionary.
# Access nested values with two sets of []:
# class_roster["student1"]["name"] -> "Alice"
# .items() on the outer dict gives (student_id, info) pairs;
# then info["name"] drills into the inner dict.
# TRY: Add "student4": {"name": "Diana", "grade": 95}
# and run the loop again.
# ======================================================
class_roster = {
"student1": {"name": "Alice", "grade": 85},
"student2": {"name": "Bob", "grade": 92},
"student3": {"name": "Charlie", "grade": 78}
}
print("Class Roster:")
for student_id, info in class_roster.items():
print(f" {student_id}: {info['name']} - Grade: {info['grade']}")
# Example 7: Practical - Phonebook
print("\n--- Example 7: Phonebook ---")
# ======================================================
# WHAT: A real use case - dictionary as a lookup table.
# Names are keys, phone numbers are values.
# Adding a new entry is just one assignment line.
# TRY: Use .get() to look up a name that is NOT in the
# phonebook and provide "Number not found" as the default.
# ======================================================
phonebook = {
"Alice": "555-1234",
"Bob": "555-5678",
"Charlie": "555-9012"
}
print("Phonebook:")
for name, number in phonebook.items():
print(f" {name}: {number}")
phonebook["Diana"] = "555-3456" # add new entry
print(f"\nAdded Diana: {phonebook['Diana']}")
# Example 8: Word counter
print("\n--- Example 8: Word Counter ---")
# ======================================================
# WHAT: Classic dictionary pattern - count occurrences.
# .split() breaks the string into a list of words.
# The loop checks: if the word is already a key,
# increment its count; otherwise start it at 1.
# This "count things" pattern appears constantly in
# real programs (log analysis, text processing, etc.)
# TRY: Change the text to a sentence you write yourself
# and see which words appear most often.
# ======================================================
text = "python is great python is fun python is popular"
words = text.split() # "python is great..." -> ["python","is","great",...]
word_count = {}
for word in words:
if word in word_count:
word_count[word] += 1 # already seen -> increment
else:
word_count[word] = 1 # first time seen -> start at 1
print("Word frequencies:")
for word, count in word_count.items():
print(f" {word}: {count}")
# ============================================
# PART 4: Sets
# ============================================
print("\n" + "=" * 50)
print("PART 4: Sets")
print("=" * 50)
# ======================================================
# WHAT THIS SECTION DOES:
# 1) PURPOSE: A set stores UNIQUE values only - duplicates
# are automatically removed. It also has no fixed order
# (items may print in a different order each time).
# Sets are great for: removing duplicates from a list,
# checking membership very fast, and doing math-like
# operations on groups (union, intersection, difference).
#
# 2) VS CODE: Run each Example with Shift+Enter. The set
# operations (Example 3) are the most powerful feature -
# study those carefully with the diagram below.
# ======================================================
# ------------------------------------------------------
# HOW SETS WORK
# ------------------------------------------------------
#
# CREATE with curly braces (no key:value, just values):
# fruits = {"apple", "banana", "cherry"}
#
# NOTE: {} alone creates an empty DICTIONARY, not a set!
# empty_set = set() <- correct empty set
# empty_dict = {} <- this is a dictionary!
#
# DUPLICATES are silently dropped:
# s = {1, 2, 2, 3, 3, 3} -> {1, 2, 3}
#
# MODIFY:
# s.add("yellow") add one item
# s.remove("green") remove (crashes if not found)
# s.discard("purple") remove (NO crash if not found)
#
# SET OPERATIONS (like Venn diagrams):
# set1 | set2 -> UNION: all items from BOTH sets
# set1 & set2 -> INTERSECTION: only items in BOTH sets
# set1 - set2 -> DIFFERENCE: items in set1 but NOT in set2
# set1 ^ set2 -> SYMMETRIC DIFFERENCE: items in ONE but not BOTH
#
# MEMBERSHIP CHECK (very fast compared to lists):
# "apple" in fruits -> True
# "grape" in fruits -> False
# ------------------------------------------------------
# Example 1: Creating sets
print("\n--- Example 1: Creating Sets ---")
# ======================================================
# WHAT: Shows that a set automatically removes duplicates.
# Converting a list with repeats to a set is the
# quickest way to get unique values in Python.
# TRY: Create a set directly with duplicates:
# {1, 2, 2, 3, 3, 3} and print it - only unique remain.
# ======================================================
fruits = {"apple", "banana", "cherry"}
numbers = {1, 2, 3, 4, 5}
print(f"Fruits: {fruits}")
print(f"Numbers: {numbers}")
numbers_list = [1, 2, 2, 3, 3, 3, 4, 4, 5]
numbers_set = set(numbers_list) # duplicates automatically removed
print(f"List with duplicates: {numbers_list}")
print(f"Set (unique only): {numbers_set}")
# Example 2: Adding and removing
print("\n--- Example 2: Adding and Removing ---")
# ======================================================
# WHAT: .add() and .remove() work like list equivalents.
# .discard() is safer than .remove() because it does
# NOT raise an error when the item is not present.
# Use discard when you are not sure if an item exists.
# TRY: Call colors.remove("purple") - you get a KeyError.
# Then replace it with colors.discard("purple") - no error.
# ======================================================
colors = {"red", "green", "blue"}
print(f"Original: {colors}")
colors.add("yellow")
print(f"After add: {colors}")
colors.remove("green") # crashes if "green" not in set
print(f"After remove: {colors}")
colors.discard("purple") # safe - no crash if not found
print(f"After discard: {colors}")
# Example 3: Set operations
print("\n--- Example 3: Set Operations ---")
# ======================================================
# WHAT: These four operations mirror Venn diagram logic:
#
# set1 = {1,2,3,4,5} set2 = {4,5,6,7,8}
#
# UNION | -> everything: {1,2,3,4,5,6,7,8}
# INTERSECTION & -> shared only: {4,5}
# DIFFERENCE - -> in set1 only: {1,2,3}
# SYMMETRIC ^ -> in one but NOT both: {1,2,3,6,7,8}
#
# TRY: Change set2 to {1, 2, 3} (identical to set1) and
# check: what does intersection return? difference?
# ======================================================
set1 = {1, 2, 3, 4, 5}
set2 = {4, 5, 6, 7, 8}
print(f"Set 1: {set1}")
print(f"Set 2: {set2}")
print(f"Union (|): {set1 | set2}") # all items from both
print(f"Intersection (&): {set1 & set2}") # only shared items
print(f"Difference (-): {set1 - set2}") # in set1 but not set2
print(f"Symmetric (^): {set1 ^ set2}") # in one but not both
# Example 4: Membership testing
print("\n--- Example 4: Membership Testing ---")
# ======================================================
# WHAT: 'in' checks whether a value exists in a set.
# Sets are much faster than lists for this check
# on large data - sets use hashing internally.
# This is one of the main reasons to choose a set.
# TRY: Compare: "apple" in fruits (set) vs
# "apple" in ["apple","banana","cherry"] (list)
# Both work - but for 1 million items, set wins easily.
# ======================================================
fruits = {"apple", "banana", "cherry"}
print(f"Fruits: {fruits}")
print(f"'apple' in fruits: {'apple' in fruits}") # True
print(f"'grape' in fruits: {'grape' in fruits}") # False
# Example 5: Practical - Remove duplicates
print("\n--- Example 5: Remove Duplicates ---")
# ======================================================
# WHAT: The quickest way to deduplicate a list in Python:
# convert to set (removes duplicates), then back to list.
# NOTE: the output order may differ from the input order
# because sets are unordered.
# TRY: Check: is the order of unique_emails guaranteed?
# Run this block twice and see if the order changes.
# ======================================================
emails = [
"alice@example.com",
"bob@example.com",
"alice@example.com", # duplicate
"charlie@example.com",
"bob@example.com" # duplicate
]
print(f"Emails with duplicates: {emails}")
unique_emails = list(set(emails)) # set removes dupes, list converts back
print(f"Unique emails: {unique_emails}")
# Example 6: Practical - Common interests
print("\n--- Example 6: Common Interests ---")
# ======================================================
# WHAT: Real-world use of set operations. Finding what two
# people have in common is just an intersection (&).
# Finding what only Alice likes is a difference (-).
# This same pattern works for: common friends,
# shared tags, overlapping skill sets, etc.
# TRY: Add "hiking" to bob_interests and re-run.
# Does "hiking" move from "only_alice" to "common"?
# ======================================================
alice_interests = {"reading", "hiking", "cooking", "music"}
bob_interests = {"music", "gaming", "hiking", "sports"}
common = alice_interests & bob_interests # shared by both
only_alice = alice_interests - bob_interests # Alice has, Bob doesn't
only_bob = bob_interests - alice_interests # Bob has, Alice doesn't
print(f"Alice's interests: {alice_interests}")
print(f"Bob's interests: {bob_interests}")
print(f"Common interests: {common}")
print(f"Only Alice: {only_alice}")
print(f"Only Bob: {only_bob}")
print("\nSession 3 completed successfully!")
print("Master these data structures for efficient programming!")