Python Tuples ā Immutable Sequences
Learning Objectives
By the end of this tutorial, you will be able to:
- Create tuples using various syntaxes and understand when to use each
- Explain why immutability matters and its practical benefits
- Use indexing, slicing, and built-in tuple methods effectively
- Master tuple unpacking including advanced patterns like star unpacking
- Understand why tuples can be dictionary keys while lists cannot
- Compare tuples and lists to choose the right data structure
- Create and use named tuples for cleaner, more readable code
- Apply tuples in common real-world programming scenarios
What Are Tuples?
A tuple is an ordered, immutable sequence in Python. Once created, its contents cannot be changed ā no adding, removing, or replacing elements.
# A simple tuple
coordinates = (10, 20)
print(coordinates) # (10, 20)
print(type(coordinates)) # <class 'tuple'>
Key Characteristics
| Characteristic | Description |
|---|---|
| Ordered | Elements maintain their position (index 0, 1, 2, ...) |
| Immutable | Cannot modify after creation |
| Allow duplicates | Same value can appear multiple times |
| Heterogeneous | Can contain different data types |
| Indexable | Access elements by position |
| Iterable | Can loop through elements |
| Hashable | Can be used as dictionary keys (if contents are hashable) |
# Tuples can hold mixed types
person = ("Alice", 30, True, 3.14)
print(person) # ('Alice', 30, True, 3.14)
# Duplicates are allowed
numbers = (1, 2, 2, 3, 3, 3)
print(numbers) # (1, 2, 2, 3, 3, 3)
Creating Tuples
Using Parentheses
The most common way to create a tuple:
# Regular tuple with multiple elements
fruits = ("apple", "banana", "cherry")
print(fruits) # ('apple', 'banana', 'cherry')
# Numbers tuple
prime_numbers = (2, 3, 5, 7, 11)
print(prime_numbers) # (2, 3, 5, 7, 11)
# Mixed types
mixed = (1, "hello", 3.14, True)
print(mixed) # (1, 'hello', 3.14, True)
Single-Element Tuple
This is the most common beginner mistake. A single element in parentheses is just a grouped expression, not a tuple:
# NOT a tuple ā this is just an integer in parentheses
not_a_tuple = (42)
print(type(not_a_tuple)) # <class 'int'>
# THIS is a tuple ā the comma makes it a tuple
single_tuple = (42,)
print(type(single_tuple)) # <class 'tuple'>
print(single_tuple) # (42,)
# Proof ā without the comma, parentheses are just grouping
print((1 + 2)) # 3 (just arithmetic)
print((1 + 2,)) # (3,) (a tuple containing the result)
Empty Tuple
# Empty tuple
empty = ()
print(type(empty)) # <class 'tuple'>
print(len(empty)) # 0
# Using the tuple() constructor for empty tuple
also_empty = tuple()
print(also_empty) # ()
Using the tuple() Constructor
# From a list
from_list = tuple([1, 2, 3])
print(from_list) # (1, 2, 3)
# From a string (each character becomes an element)
from_string = tuple("Python")
print(from_string) # ('P', 'y', 't', 'h', 'o', 'n')
# From a range
from_range = tuple(range(5))
print(from_range) # (0, 1, 2, 3, 4)
# From a set (order may vary since sets are unordered)
from_set = tuple({3, 1, 4, 1, 5})
print(from_set) # (1, 3, 4, 5) or similar
From Other Iterables
# Generator expression
squares = tuple(x**2 for x in range(5))
print(squares) # (0, 1, 4, 9, 16)
# Map function
lengths = tuple(map(len, ["hello", "world", "python"]))
print(lengths) # (5, 5, 6)
# Filter function
evens = tuple(filter(lambda x: x % 2 == 0, range(10)))
print(evens) # (0, 2, 4, 6, 8)
Why Immutability Matters
Immutability is not a limitation ā it's a feature with significant benefits.
Thread Safety
Immutable objects are inherently thread-safe. Multiple threads can read the same tuple without locks:
import threading
# Shared data that won't change
CONFIG = ("localhost", 8080, "admin")
def read_config():
# Safe ā CONFIG cannot be modified by another thread
host, port, user = CONFIG
print(f"Connecting to {host}:{port} as {user}")
# Multiple threads can safely access CONFIG
for _ in range(3):
t = threading.Thread(target=read_config)
t.start()
Dictionary Keys
Tuples can be dictionary keys; lists cannot:
# Tuples as dictionary keys
distances = {
(0, 0): "origin",
(1, 2): "point A",
(3, 4): "point B"
}
print(distances[(1, 2)]) # point A
# Lists CANNOT be dictionary keys ā this raises TypeError
# distances = {[0, 0]: "origin"} # TypeError: unhashable type: 'list'
Performance Advantages
Tuples are generally faster than lists for fixed data:
import sys
# Memory comparison
my_list = [1, 2, 3, 4, 5]
my_tuple = (1, 2, 3, 4, 5)
print(sys.getsizeof(my_list)) # 96 bytes (typical)
print(sys.getsizeof(my_tuple)) # 80 bytes (typical)
# Creation speed comparison
import timeit
list_time = timeit.timeit('[1, 2, 3, 4, 5]', number=1000000)
tuple_time = timeit.timeit('(1, 2, 3, 4, 5)', number=1000000)
print(f"List creation: {list_time:.4f}s")
print(f"Tuple creation: {tuple_time:.4f}s")
# Tuple creation is typically faster
Memory Efficiency
import sys
# A tuple uses less memory than a list with the same elements
data_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
data_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
print(f"List size: {sys.getsizeof(data_list)} bytes") # ~120 bytes
print(f"Tuple size: {sys.getsizeof(data_tuple)} bytes") # ~104 bytes
Data Integrity
Immutability guarantees that data won't be accidentally modified:
# This is a guarantee that the data remains unchanged
WEEKDAYS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
# No code can accidentally do this:
# WEEKDAYS[0] = "X" # TypeError: 'tuple' object does not support item assignment
# This makes tuples ideal for:
# - Configuration constants
# - Database keys
# - Coordinate pairs
# - Any data that should not change
Indexing and Slicing
Tuples use the same indexing and slicing syntax as lists:
Basic Indexing
colors = ("red", "green", "blue", "yellow", "purple")
# Positive indexing (starts at 0)
print(colors[0]) # red
print(colors[2]) # blue
print(colors[-1]) # purple (last element)
print(colors[-2]) # yellow (second to last)
# Accessing nested tuples
matrix = ((1, 2), (3, 4), (5, 6))
print(matrix[0]) # (1, 2)
print(matrix[0][1]) # 2
print(matrix[1][0]) # 3
Slicing
numbers = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
# Basic slicing: tuple[start:stop]
print(numbers[2:5]) # (2, 3, 4)
print(numbers[:4]) # (0, 1, 2, 3)
print(numbers[6:]) # (6, 7, 8, 9)
# Slicing with step: tuple[start:stop:step]
print(numbers[::2]) # (0, 2, 4, 6, 8) ā every other element
print(numbers[1::2]) # (1, 3, 5, 7, 9) ā every other from index 1
print(numbers[::-1]) # (9, 8, 7, 6, 5, 4, 3, 2, 1, 0) ā reversed
# Slicing always returns a NEW tuple
sliced = numbers[2:5]
print(type(sliced)) # <class 'tuple'>
print(sliced) # (2, 3, 4)
Immutability in Action
point = (10, 20, 30)
# This works ā creates a new tuple
new_point = point + (40,)
print(new_point) # (10, 20, 30, 40)
# This FAILS ā you cannot modify in place
# point[0] = 99 # TypeError: 'tuple' object does not support item assignment
# Concatenation creates a new tuple
tuple_a = (1, 2)
tuple_b = (3, 4)
combined = tuple_a + tuple_b
print(combined) # (1, 2, 3, 4)
print(tuple_a) # (1, 2) ā original unchanged
# Repetition creates a new tuple
repeated = (0,) * 5
print(repeated) # (0, 0, 0, 0, 0)
Tuple Methods
Tuples have only two built-in methods due to their immutability:
count()
Returns the number of occurrences of a value:
numbers = (1, 2, 3, 2, 4, 2, 5, 2)
print(numbers.count(2)) # 4
print(numbers.count(3)) # 1
print(numbers.count(6)) # 0 (not found)
# Practical use: counting character frequency
text = tuple("mississippi")
print(text.count('s')) # 4
print(text.count('i')) # 4
print(text.count('m')) # 1
index()
Returns the index of the first occurrence of a value:
colors = ("red", "green", "blue", "yellow", "green")
print(colors.index("green")) # 1
print(colors.index("blue")) # 2
# Raises ValueError if not found
try:
print(colors.index("purple"))
except ValueError as e:
print(f"Error: {e}") # Error: 'purple' is not in tuple
# Using start and stop parameters
print(colors.index("green", 2)) # 4 (searches from index 2 onwards)
Why Fewer Methods Than Lists?
Lists have many more methods (append, extend, insert, remove, pop, sort, reverse, etc.) because they are mutable. Since tuples cannot be modified, they only need methods for querying data:
# Lists have these mutation methods ā tuples don't:
# list.append(x) ā add element
# list.extend(x) ā add multiple elements
# list.insert(i, x) ā insert at position
# list.remove(x) ā remove first occurrence
# list.pop() ā remove and return
# list.sort() ā sort in place
# list.reverse() ā reverse in place
# list.clear() ā remove all elements
# Tuples only have:
# tuple.count(x) ā count occurrences
# tuple.index(x) ā find index
Tuple Unpacking
Unpacking is one of the most powerful and Pythonic features of tuples.
Basic Unpacking
Assign tuple elements to individual variables:
# Simple unpacking
point = (10, 20)
x, y = point
print(f"x = {x}, y = {y}") # x = 10, y = 20
# Three elements
rgb = (255, 128, 0)
red, green, blue = rgb
print(f"Red: {red}, Green: {green}, Blue: {blue}")
# Works with any iterable
a, b, c = [1, 2, 3] # Also works with lists
print(a, b, c) # 1 2 3
# Must have exactly the right number of values
# a, b = (1, 2, 3) # ValueError: too many values to unpack
# a, b, c, d = (1, 2) # ValueError: not enough values to unpack
Star Unpacking
Use * to capture multiple elements into a list:
# Capture first and rest
first, *rest = (1, 2, 3, 4, 5)
print(first) # 1
print(rest) # [2, 3, 4, 5]
# Capture last and everything before
*body, last = (1, 2, 3, 4, 5)
print(body) # [1, 2, 3, 4]
print(last) # 5
# Capture first, middle, and last
first, *middle, last = (1, 2, 3, 4, 5)
print(first) # 1
print(middle) # [2, 3, 4]
print(last) # 5
# Only one star expression allowed per unpacking
# *a, *b = (1, 2, 3) # SyntaxError: multiple starred expressions
Swapping Variables
Pythonic way to swap two variables:
# Traditional swap (using temporary variable)
a = 5
b = 10
temp = a
a = b
b = temp
# Pythonic swap using tuple unpacking
a, b = b, a
print(f"a = {a}, b = {b}") # a = 10, b = 5
# Works with multiple variables
x, y, z = 1, 2, 3
x, y, z = z, x, y
print(x, y, z) # 3 1 2
Ignoring Values
Use _ for values you don't need:
# Ignore the year, keep month and day
date = (2025, 3, 15)
_, month, day = date
print(f"Month: {month}, Day: {day}") # Month: 3, Day: 15
# Ignore first and last
first, *_, last = (1, 2, 3, 4, 5)
print(f"First: {first}, Last: {last}") # First: 1, Last: 5
# Ignore multiple values
_, _, third, *_ = (1, 2, 3, 4, 5, 6)
print(third) # 3
Nested Unpacking
Unpack tuples containing other tuples:
# Nested tuple
person = ("Alice", (25, "Engineer"))
name, (age, job) = person
print(f"{name}, {age}, {job}") # Alice, 25, Engineer
# Deeply nested
company = ("TechCorp", ("Engineering", ("Python", "Backend")))
name, (dept, (lang, role)) = company
print(f"{name} - {dept} - {lang} - {role}")
# Matrix unpacking
matrix = ((1, 2, 3), (4, 5, 6), (7, 8, 9))
(r1c1, r1c2, r1c3), (r2c1, r2c2, r2c3), (r3c1, r3c2, r3c3) = matrix
print(f"Center element: {r2c2}") # 5
Unpacking in Loops
# Iterating over pairs
pairs = [(1, "a"), (2, "b"), (3, "c")]
for number, letter in pairs:
print(f"{number} -> {letter}")
# Iterating over dictionary items (which are tuples)
person = {"name": "Alice", "age": 30, "city": "NYC"}
for key, value in person.items():
print(f"{key}: {value}")
# Using enumerate (returns tuples of index, value)
fruits = ["apple", "banana", "cherry"]
for index, fruit in enumerate(fruits):
print(f"{index}: {fruit}")
# Zipping (returns tuples)
names = ("Alice", "Bob", "Charlie")
scores = (95, 87, 92)
for name, score in zip(names, scores):
print(f"{name}: {score}")
Tuples as Dictionary Keys
Why Lists Can't Be Keys
Dictionary keys must be hashable ā they need a fixed hash value that doesn't change. Lists are mutable, so their hash can change:
# Lists are NOT hashable
try:
d = {[1, 2]: "value"} # TypeError: unhashable type: 'list'
except TypeError as e:
print(f"Error: {e}")
# Lists don't have a __hash__ method
print(hash([1, 2])) # TypeError: unhashable type: 'list'
Hashability Explained
# Immutable types are hashable
print(hash(42)) # 42
print(hash("hello")) # some integer
print(hash((1, 2))) # some integer
# Mutable types are NOT hashable
# hash([1, 2]) # TypeError
# hash({1: "a"}) # TypeError
# Tuples with mutable contents are also NOT hashable
try:
hash((1, [2, 3])) # TypeError: unhashable type: 'list'
except TypeError as e:
print(f"Error: {e}")
# Tuples with only immutable contents ARE hashable
print(hash((1, 2, 3))) # works
print(hash((1, "hello", (2, 3))) # works
Using Tuples as Composite Keys
# Grid/cell coordinates
grid = {}
grid[(0, 0)] = "origin"
grid[(1, 0)] = "right"
grid[(0, 1)] = "up"
print(grid[(1, 0)]) # right
# Sparse matrix (only store non-zero values)
sparse = {}
sparse[(0, 0)] = 5
sparse[(2, 3)] = 10
sparse[(5, 1)] = 7
print(sparse[(2, 3)]) # 10
# Multi-dimensional lookup table
lookup = {
(True, True): "both true",
(True, False): "first true",
(False, True): "second true",
(False, False): "both false"
}
print(lookup[(True, False)]) # first true
# Caching with tuple keys
cache = {}
def expensive_calc(x, y):
key = (x, y)
if key not in cache:
cache[key] = x ** y # expensive operation
return cache[key]
print(expensive_calc(2, 10)) # 1024
print(expensive_calc(2, 10)) # 1024 (from cache)
Tuples vs Lists ā Comprehensive Comparison
| Feature | Tuple | List |
|---|---|---|
| Syntax | (1, 2, 3) | [1, 2, 3] |
| Mutability | Immutable | Mutable |
| Speed | Faster to create and access | Slightly slower |
| Memory | Uses less memory | Uses more memory |
| Dictionary keys | Yes (if hashable) | No |
| Methods | count(), index() | append, extend, insert, remove, pop, sort, reverse, clear, copy, count, index |
| Use case | Fixed data, dictionary keys | Dynamic collections |
| Iteration | Slightly faster | Slightly slower |
| Copy | Not needed (immutable) | Need to copy |
| Best for | Heterogeneous data | Homogeneous data |
When to Use Tuples
# 1. Fixed collections (coordinates, RGB values)
point = (10, 20)
color = (255, 128, 0)
# 2. Dictionary keys
locations = {(40.7128, -74.0060): "New York City"}
# 3. Function return values (multiple values)
def get_min_max(numbers):
return min(numbers), max(numbers)
lo, hi = get_min_max([3, 1, 4, 1, 5, 9])
print(f"Min: {lo}, Max: {hi}")
# 4. Unpacking in for loops
for x, y in [(1, 2), (3, 4), (5, 6)]:
print(x + y)
# 5. String formatting with % operator
print("Hello %s, you are %d years old" % ("Alice", 30))
# 6. Struct-like records
record = ("Alice", 30, "Engineer")
name, age, title = record
# 7. Database rows
row = (1, "Alice", "alice@example.com")
When to Use Lists
# 1. Dynamic collections that change
shopping_list = ["milk", "eggs"]
shopping_list.append("bread")
# 2. Homogeneous data
numbers = [1, 2, 3, 4, 5]
numbers.append(6)
# 3. Data that needs sorting
scores = [85, 92, 78, 95, 88]
scores.sort()
# 4. Stacks and queues
stack = []
stack.append(1)
stack.append(2)
stack.pop()
Named Tuples (Preview)
Named tuples provide a way to create tuple-like objects with named fields:
from collections import namedtuple
# Define a named tuple type
Point = namedtuple('Point', ['x', 'y'])
# Create instances
p = Point(10, 20)
print(p) # Point(x=10, y=20)
print(p.x) # 10
print(p.y) # 20
# Still a tuple ā supports indexing
print(p[0]) # 10
print(p[1]) # 20
# Unpacking works
x, y = p
print(f"x={x}, y={y}")
# Immutable
try:
p.x = 30 # AttributeError: can't set attribute
except AttributeError as e:
print(f"Error: {e}")
Named Tuple for Readability
from collections import namedtuple
# Instead of plain tuples:
# person = ("Alice", 30, "alice@example.com")
# Use named tuples for clarity:
Person = namedtuple('Person', ['name', 'age', 'email'])
alice = Person("Alice", 30, "alice@example.com")
print(f"Name: {alice.name}") # Name: Alice
print(f"Age: {alice.age}") # Age: 30
print(f"Email: {alice.email}") # Email: alice@example.com
# As a lightweight class (no __init__ boilerplate)
Employee = namedtuple('Employee', ['id', 'name', 'department', 'salary'])
emp = Employee(1, "Bob", "Engineering", 95000)
print(f"{emp.name} works in {emp.department}") # Bob works in Engineering
Named Tuples with Defaults
from collections import namedtuple
# Python 3.6.1+
Config = namedtuple('Config', ['host', 'port', 'debug'], defaults=['localhost', 8080, False])
# Use defaults
c1 = Config()
print(c1) # Config(host='localhost', port=8080, debug=False)
# Override some defaults
c2 = Config(host='example.com', port=9000)
print(c2) # Config(host='example.com', port=9000, debug=False)
# Override all
c3 = Config('0.0.0.0', 3000, True)
print(c3) # Config(host='0.0.0.0', port=3000, debug=True)
As Lightweight Classes
from collections import namedtuple
# A simple class without writing __init__, __repr__, __eq__
Color = namedtuple('Color', ['red', 'green', 'blue', 'alpha'])
# Create color
red = Color(255, 0, 0, 1.0)
print(f"Red: {red.red}") # 255
# Convert to dictionary
color_dict = red._asdict()
print(color_dict) # {'red': 255, 'green': 0, 'blue': 0, 'alpha': 1.0}
# Create a modified version
transparent_red = red._replace(alpha=0.5)
print(transparent_red) # Color(red=255, green=0, blue=0, alpha=0.5)
# Original unchanged
print(red.alpha) # 1.0
Common Use Cases
Function Return Values
# Returning multiple values from a function
def get_stats(numbers):
return min(numbers), max(numbers), sum(numbers) / len(numbers)
lo, hi, avg = get_stats([10, 20, 30, 40, 50])
print(f"Min: {lo}, Max: {hi}, Avg: {avg}")
# Returning success/failure with data
def divide(a, b):
if b == 0:
return False, None
return True, a / b
success, result = divide(10, 3)
if success:
print(f"Result: {result:.2f}")
else:
print("Cannot divide by zero")
Swapping Variables
# Swap list elements
def swap_first_last(lst):
first, *middle, last = lst
return [last] + middle + [first]
print(swap_first_last([1, 2, 3, 4, 5])) # [5, 2, 3, 4, 1]
# Rotate a tuple
def rotate_right(t):
if len(t) <= 1:
return t
return (t[-1],) + t[:-1]
print(rotate_right((1, 2, 3, 4))) # (4, 1, 2, 3)
Dictionary Items
# Iterating over dictionary items
student_grades = {"Alice": 95, "Bob": 87, "Charlie": 92}
for name, grade in student_grades.items():
print(f"{name}: {grade}")
# Creating a dictionary from tuples
pairs = [("a", 1), ("b", 2), ("c", 3)]
d = dict(pairs)
print(d) # {'a': 1, 'b': 2, 'c': 3}
# Sorting dictionary by value
sorted_pairs = sorted(student_grades.items(), key=lambda x: x[1], reverse=True)
print(sorted_pairs) # [('Alice', 95), ('Charlie', 92), ('Bob', 87)]
Database Rows
# Simulating database results
def fetch_users():
# Returns list of tuples (like database rows)
return [
(1, "Alice", "alice@email.com"),
(2, "Bob", "bob@email.com"),
(3, "Charlie", "charlie@email.com"),
]
for user_id, name, email in fetch_users():
print(f"User {user_id}: {name} <{email}>")
Coordinates and Structured Data
# Working with coordinates
def manhattan_distance(p1, p2):
x1, y1 = p1
x2, y2 = p2
return abs(x1 - x2) + abs(y1 - y2)
print(manhattan_distance((0, 0), (3, 4))) # 7
# Working with bounding boxes
def bbox_area(box):
"""box = (x_min, y_min, x_max, y_max)"""
x_min, y_min, x_max, y_max = box
return (x_max - x_min) * (y_max - y_min)
print(bbox_area((0, 0, 10, 5))) # 50
# RGB color operations
def blend_colors(c1, c2, ratio=0.5):
"""Blend two RGB tuples."""
r1, g1, b1 = c1
r2, g2, b2 = c2
return (
int(r1 * (1 - ratio) + r2 * ratio),
int(g1 * (1 - ratio) + g2 * ratio),
int(b1 * (1 - ratio) + b2 * ratio),
)
red = (255, 0, 0)
blue = (0, 0, 255)
purple = blend_colors(red, blue)
print(purple) # (127, 0, 127)
Common Mistakes
Mistake 1: Forgetting the Comma for Single-Element Tuples
# WRONG ā not a tuple
not_a_tuple = (42)
print(type(not_a_tuple)) # <class 'int'>
# RIGHT ā comma makes it a tuple
is_a_tuple = (42,)
print(type(is_a_tuple)) # <class 'tuple'>
# Also works without parentheses
also_tuple = 42,
print(type(also_tuple)) # <class 'tuple'>
print(also_tuple) # (42,)
Mistake 2: Trying to Modify Tuple Elements
colors = ("red", "green", "blue")
# WRONG ā this fails
try:
colors[0] = "purple" # TypeError
except TypeError as e:
print(f"Error: {e}")
# RIGHT ā create a new tuple instead
colors = ("red", "green", "blue")
new_colors = ("purple",) + colors[1:]
print(new_colors) # ('purple', 'green', 'blue')
Mistake 3: Confusing () with Empty Tuple vs Grouping
# Empty tuple
empty = ()
print(type(empty)) # <class 'tuple'>
# Grouping (not a tuple)
grouped = (42)
print(type(grouped)) # <class 'int'>
# Multiple elements ā parentheses are optional
no_parens = 1, 2, 3
with_parens = (1, 2, 3)
print(type(no_parens)) # <class 'tuple'>
print(type(with_parens)) # <class 'tuple'>
# But parentheses ARE needed for function returns and comprehensions
# return 1, 2 # Returns a tuple
# return (1, 2) # Also returns a tuple ā same thing
Mistake 4: Shallow vs Deep Copy
import copy
# Tuples are immutable, but nested objects may be mutable
original = ([1, 2], [3, 4])
# Assignment ā NOT a copy
alias = original
alias[0][0] = 99
print(original) # ([99, 2], [3, 4]) ā original changed!
# Shallow copy
original = ([1, 2], [3, 4])
shallow = copy.copy(original)
shallow[0][0] = 99
print(original) # ([99, 2], [3, 4]) ā still changed (nested lists shared)
# Deep copy
original = ([1, 2], [3, 4])
deep = copy.deepcopy(original)
deep[0][0] = 99
print(original) # ([1, 2], [3, 4]) ā original preserved
Mistake 5: Using Tuples When Lists Are Better
# Bad: Using tuple for growing data
data = ()
for i in range(10):
data = data + (i,) # Creates new tuple each time ā O(n²)!
# Good: Using list for growing data
data = []
for i in range(10):
data.append(i) # O(1) amortized
# Convert to tuple at the end if needed
data = tuple(data)
Practice Exercises
Exercise 1: Tuple Manipulation
Write a function that takes a tuple and returns a new tuple with the first and last elements swapped.
# Write your function here
def swap_ends(t):
if len(t) <= 1:
return t
return (t[-1],) + t[1:-1] + (t[0],)
# Test cases
print(swap_ends((1, 2, 3, 4))) # (4, 2, 3, 1)
print(swap_ends(("a", "b", "c"))) # ('c', 'b', 'a')
print(swap_ends((42,))) # (42,)
print(swap_ends(())) # ()
Solution
def swap_ends(t):
if len(t) <= 1:
return t
return (t[-1],) + t[1:-1] + (t[0],)
# Test cases
print(swap_ends((1, 2, 3, 4))) # (4, 2, 3, 1)
print(swap_ends(("a", "b", "c"))) # ('c', 'b', 'a')
print(swap_ends((42,))) # (42,)
print(swap_ends(())) # ()
Exercise 2: Tuple Unpacking Challenge
Write code that unpacks a list of tuples containing student names and scores, and prints the top 3 students.
students = [
("Alice", 95),
("Bob", 87),
("Charlie", 92),
("Diana", 98),
("Eve", 89),
("Frank", 91),
]
# Write your code here
sorted_students = sorted(students, key=lambda s: s[1], reverse=True)
for rank, (name, score) in enumerate(sorted_students[:3], 1):
print(f"#{rank}: {name} ({score})")
# Expected output:
# #1: Diana (98)
# #2: Alice (95)
# #3: Charlie (92)
Solution
students = [
("Alice", 95),
("Bob", 87),
("Charlie", 92),
("Diana", 98),
("Eve", 89),
("Frank", 91),
]
sorted_students = sorted(students, key=lambda s: s[1], reverse=True)
for rank, (name, score) in enumerate(sorted_students[:3], 1):
print(f"#{rank}: {name} ({score})")
# Output:
# #1: Diana (98)
# #2: Alice (95)
# #3: Charlie (92)
Exercise 3: Named Tuple Calculator
Create a named tuple Operation with fields operator, operand1, operand2, and write a function that evaluates it.
from collections import namedtuple
# Define the named tuple
Operation = namedtuple('Operation', ['operator', 'operand1', 'operand2'])
def evaluate(op):
# Write your code here
if op.operator == '+':
return op.operand1 + op.operand2
elif op.operator == '-':
return op.operand1 - op.operand2
elif op.operator == '*':
return op.operand1 * op.operand2
elif op.operator == '/':
if op.operand2 == 0:
return "Error: Division by zero"
return op.operand1 / op.operand2
else:
return f"Error: Unknown operator '{op.operator}'"
# Test
op1 = Operation('+', 10, 5)
op2 = Operation('*', 3, 4)
op3 = Operation('/', 20, 0)
print(f"{op1.operand1} {op1.operator} {op1.operand2} = {evaluate(op1)}")
print(f"{op2.operand1} {op2.operator} {op2.operand2} = {evaluate(op2)}")
print(f"{op3.operand1} {op3.operator} {op3.operand2} = {evaluate(op3)}")
Solution
from collections import namedtuple
Operation = namedtuple('Operation', ['operator', 'operand1', 'operand2'])
def evaluate(op):
if op.operator == '+':
return op.operand1 + op.operand2
elif op.operator == '-':
return op.operand1 - op.operand2
elif op.operator == '*':
return op.operand1 * op.operand2
elif op.operator == '/':
if op.operand2 == 0:
return "Error: Division by zero"
return op.operand1 / op.operand2
else:
return f"Error: Unknown operator '{op.operator}'"
op1 = Operation('+', 10, 5)
op2 = Operation('*', 3, 4)
op3 = Operation('/', 20, 0)
print(f"{op1.operand1} {op1.operator} {op1.operand2} = {evaluate(op1)}")
print(f"{op2.operand1} {op2.operator} {op2.operand2} = {evaluate(op2)}")
print(f"{op3.operand1} {op3.operator} {op3.operand2} = {evaluate(op3)}")
# Output:
# 10 + 5 = 15
# 3 * 4 = 12
# 20 / 0 = Error: Division by zero
Key Takeaways
- Tuples are immutable ā once created, their contents cannot change, which provides safety, performance, and hashability.
- The comma matters ā
(x,)is a tuple,(x)is justxin parentheses. Always use a trailing comma for single-element tuples. - Unpacking is powerful ā use
a, b = (1, 2)for basic unpacking,first, *rest = (1, 2, 3)for star unpacking, and_to ignore values. - Tuples can be dictionary keys ā because they are hashable (if all elements are hashable), unlike lists.
- Use tuples for fixed data ā coordinates, RGB values, database rows, function return values, and any collection that shouldn't change.
- Use lists for dynamic data ā when you need to add, remove, or modify elements.
- Named tuples add clarity ā use
collections.namedtuplewhen you want tuple benefits with readable field names instead of magic indices. - Two methods only ā
count()andindex(), because immutability means no modification methods are needed. - Nested tuples with mutable contents are not hashable ā a tuple containing a list cannot be a dictionary key.
- Prefer tuples over lists for heterogeneous data ā tuples are the Pythonic choice for records with different field types.
Next: Learn about Python Dictionaries ā key-value pairs for fast lookups and real-world data modeling.