Tuples and Sets - Special Collection Types

Tuples are like family photos - once taken, you can't change who's in them (immutable). Sets are like a basket of unique fruits - you can't have two identical apples, and the order doesn't matter.
Python provides two specialized collection types beyond lists and dictionaries: **tuples** and **sets**. Each serves a unique purpose and is optimised for specific tasks.

## TUPLES (Immutable Sequences)

A **tuple** is an ordered collection of items, just like a list, but **immutable** – once created, you cannot change, add, or remove items. This makes tuples ideal for data that should never change, such as coordinates, RGB color values, or records from a database.

**Creating a tuple:**
Use parentheses `()` with items separated by commas.
```python
coordinates = (10, 20)
person = ("Alice", 25, "Engineer")
single_item_tuple = (42,) # note the trailing comma – required!
```

**Accessing elements:**
Like lists, tuples support indexing and slicing (zero‑based).
```python
print(person[0]) # 'Alice'
print(person[-1]) # 'Engineer'
```

**Tuple unpacking:**
Assign each element of a tuple to a separate variable in one line.
```python
name, age, job = person
print(name) # Alice
```

**Why use tuples instead of lists?**
- **Immutability** – ensures data integrity (e.g., dictionary keys, function returns).
- **Performance** – tuples are slightly faster than lists.
- **Hashable** – tuples can be used as dictionary keys (lists cannot).
- **Fixed data** – coordinates, settings, constants.

**Tuples as dictionary keys:**
Because tuples are immutable, they can be used as keys in dictionaries. Lists cannot.
```python
locations = {(40.7128, -74.0060): "New York", (51.5074, -0.1278): "London"}
```

**Methods:**
Tuples have only two methods: `.count(value)` and `.index(value)`. No `.append()`, `.remove()`, or `.sort()`.

## SETS (Unordered Collections of Unique Elements)

A **set** is an unordered collection of unique items. Duplicates are automatically removed. Sets are optimised for fast membership testing (`in` operator) and mathematical set operations.

**Creating a set:**
Use curly braces `{}` or the `set()` constructor.
```python
fruits = {"apple", "banana", "cherry"}
numbers = set([1, 2, 2, 3]) # {1, 2, 3}
empty_set = set() # {} creates an empty dict, not set!
```

**Basic operations:**
- `.add(item)` – adds an item (ignores if already present).
- `.remove(item)` – removes item; raises `KeyError` if not found.
- `.discard(item)` – removes if present; no error if missing.
- `.pop()` – removes and returns an arbitrary element.
- `.clear()` – removes all elements.

**Set operations (mathematical):**
- **Union** `|` or `.union()` – all elements from both sets.
- **Intersection** `&` or `.intersection()` – elements common to both.
- **Difference** `-` or `.difference()` – elements in first but not second.
- **Symmetric difference** `^` or `.symmetric_difference()` – elements in either, but not both.

**Membership testing:**
Sets are extremely fast for `in` checks (O(1) average).
```python
vowels = {'a', 'e', 'i', 'o', 'u'}
if 'e' in vowels:
print("Vowel!")
```

**Common use cases for sets:**
- Removing duplicates from a list (`unique = set(my_list)`).
- Checking for common elements between collections.
- Tracking visited items (e.g., in web crawlers).
- Fast existence checks.

**Important differences between sets and lists:**
- Sets are **unordered** – you cannot rely on insertion order (though recent Python preserves order, you shouldn't depend on it).
- Sets only store **hashable** (immutable) items. You cannot put a list or another set inside a set, but you can put a tuple.
- Sets do not support indexing (`my_set[0]` is invalid).

**Frozen sets (immutable sets):**
`frozenset()` creates an immutable version of a set, which can be used as a dictionary key.

**Practice exercises:**
1. Create a tuple with your name, age, and city. Unpack it into variables and print each.
2. Write a function that returns the minimum and maximum of a list as a tuple.
3. Given two lists, find the common elements using sets.
4. Remove duplicates from a list while preserving order (hint: use a set and a loop).
5. Check if two strings are anagrams using sets (ignore case and spaces).

**When to choose which collection:**
- **List** – ordered, mutable, allows duplicates, needs indexing.
- **Tuple** – ordered, immutable, fixed data, dictionary keys.
- **Set** – unordered, mutable, unique items, fast membership.
- **Dictionary** – key‑value pairs.

**Real‑world examples:**
- **Tuples**: RGB colors, coordinates, database rows, function returning multiple values.
- **Sets**: Unique visitors to a website, tags on a blog post, filtering duplicates, friend recommendations (common interests).
# ========== EXAMPLE 1: Tuples – Creation, Unpacking, and Immutability ==========
print("=== Example 1: Working with Tuples ===")
# Creating tuples
coordinates = (10, 20)
colors = ("red", "green", "blue")
person = ("Alice", 25, "Engineer")

print(f"Coordinates: {coordinates}")
print(f"First color: {colors[0]}")
print(f"Person: Name={person[0]}, Age={person[1]}, Job={person[2]}")

# Tuple unpacking
x, y = coordinates
print(f"\nUnpacked coordinates: x={x}, y={y}")

name, age, job = person
print(f"Unpacked person: {name} is {age} years old, works as {job}")

# Functions returning tuples
def get_min_max(numbers):
    return min(numbers), max(numbers)

scores = [85, 92, 78, 90, 88]
lowest, highest = get_min_max(scores)
print(f"\nScores: {scores}")
print(f"Lowest: {lowest}, Highest: {highest}")
print()

# ========== EXAMPLE 2: Sets – Creation, Uniqueness, and Basic Operations ==========
print("=== Example 2: Sets – Uniqueness and Operations ===")
# Sets automatically remove duplicates
fruits = {"apple", "banana", "orange", "apple"}
numbers = set([1, 2, 3, 2, 1])
print(f"Fruits set (no duplicates): {fruits}")
print(f"Numbers set (no duplicates): {numbers}")

# Adding and removing
fruits.add("grape")
print(f"After add: {fruits}")
fruits.discard("banana")   # no error if missing
print(f"After discard: {fruits}")

# Set operations
set_a = {1, 2, 3, 4, 5}
set_b = {4, 5, 6, 7, 8}
print(f"\nSet A: {set_a}")
print(f"Set B: {set_b}")
print(f"Union (A | B): {set_a | set_b}")
print(f"Intersection (A & B): {set_a & set_b}")
print(f"Difference (A - B): {set_a - set_b}")
print()

# ========== EXAMPLE 3: Practical Uses – Removing Duplicates and Fast Membership ==========
print("=== Example 3: Real‑World Set Applications ===")
# Removing duplicates from a list
grades = [85, 90, 85, 88, 90, 92, 85]
unique_grades = list(set(grades))
print(f"Original grades: {grades}")
print(f"Unique grades (using set): {unique_grades}")

# Fast membership testing
vowels = {'a', 'e', 'i', 'o', 'u'}
word = "hello"
found_vowels = [letter for letter in word if letter in vowels]
print(f"\nWord: '{word}'")
print(f"Vowels found: {found_vowels}")

# Checking common elements between two lists
list1 = ["math", "science", "history"]
list2 = ["science", "art", "math"]
common = set(list1) & set(list2)
print(f"\nCommon subjects: {common}")

# Tuples as dictionary keys (immutability advantage)
location_map = {
    (40.7128, -74.0060): "New York",
    (51.5074, -0.1278): "London",
    (35.6762, 139.6503): "Tokyo"
}
print(f"\nCoordinates (40.7128, -74.0060): {location_map[(40.7128, -74.0060)]}")

→ Run this code interactively