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 all related built-in functions:
len(),min(),max(),sum(),sorted(),reversed(),enumerate(),zip()
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.
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) |
person = ("Alice", 30, True, 3.14)
print(person) # ('Alice', 30, True, 3.14)
numbers = (1, 2, 2, 3, 3, 3)
print(numbers) # (1, 2, 2, 3, 3, 3)
Creating Tuples
Using Parentheses
fruits = ("apple", "banana", "cherry")
print(fruits) # ('apple', 'banana', 'cherry')
prime_numbers = (2, 3, 5, 7, 11)
print(prime_numbers) # (2, 3, 5, 7, 11)
mixed = (1, "hello", 3.14, True)
print(mixed) # (1, 'hello', 3.14, True)
Single-Element Tuple
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
print((1 + 2)) # 3 (just arithmetic)
print((1 + 2,)) # (3,) (a tuple containing the result)
Empty Tuple
empty = ()
print(type(empty)) # <class 'tuple'>
print(len(empty)) # 0
also_empty = tuple()
print(also_empty) # ()
Using the tuple() Constructor
from_list = tuple([1, 2, 3])
print(from_list) # (1, 2, 3)
from_string = tuple("Python")
print(from_string) # ('P', 'y', 't', 'h', 'o', 'n')
from_range = tuple(range(5))
print(from_range) # (0, 1, 2, 3, 4)
squares = tuple(x**2 for x in range(5))
print(squares) # (0, 1, 4, 9, 16)
Why Immutability Matters
Thread Safety
import threading
CONFIG = ("localhost", 8080, "admin")
def read_config():
host, port, user = CONFIG
print(f"Connecting to {host}:{port} as {user}")
for _ in range(3):
t = threading.Thread(target=read_config)
t.start()
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
# distances = {[0, 0]: "origin"} # TypeError: unhashable type: 'list'
Performance Advantages
import sys
my_list = [1, 2, 3, 4, 5]
my_tuple = (1, 2, 3, 4, 5)
print(sys.getsizeof(my_list)) # 96 bytes
print(sys.getsizeof(my_tuple)) # 80 bytes
Data Integrity
WEEKDAYS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
# WEEKDAYS[0] = "X" # TypeError: 'tuple' object does not support item assignment
Indexing and Slicing
Basic Indexing
colors = ("red", "green", "blue", "yellow", "purple")
print(colors[0]) # red
print(colors[2]) # blue
print(colors[-1]) # purple
print(colors[-2]) # yellow
# 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)
print(numbers[2:5]) # (2, 3, 4)
print(numbers[:4]) # (0, 1, 2, 3)
print(numbers[6:]) # (6, 7, 8, 9)
print(numbers[::2]) # (0, 2, 4, 6, 8)
print(numbers[1::2]) # (1, 3, 5, 7, 9)
print(numbers[::-1]) # (9, 8, 7, 6, 5, 4, 3, 2, 1, 0)
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
# point[0] = 99 # TypeError
# 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) — 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
# 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}")
# Using start and stop parameters
print(colors.index("green", 2)) # 4 (searches from index 2 onwards)
Tuple Unpacking
Basic Unpacking
point = (10, 20)
x, y = point
print(f"x = {x}, y = {y}") # x = 10, y = 20
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]
print(a, b, c) # 1 2 3
Star Unpacking
# 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
Swapping Variables
a, b = b, a # Pythonic swap
x, y, z = 1, 2, 3
x, y, z = z, x, y
print(x, y, z) # 3 1 2
Ignoring Values
date = (2025, 3, 15)
_, month, day = date
print(f"Month: {month}, Day: {day}") # Month: 3, Day: 15
first, *_, last = (1, 2, 3, 4, 5)
print(f"First: {first}, Last: {last}") # First: 1, Last: 5
Nested Unpacking
person = ("Alice", (25, "Engineer"))
name, (age, job) = person
print(f"{name}, {age}, {job}") # Alice, 25, Engineer
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
pairs = [(1, "a"), (2, "b"), (3, "c")]
for number, letter in pairs:
print(f"{number} -> {letter}")
person = {"name": "Alice", "age": 30, "city": "NYC"}
for key, value in person.items():
print(f"{key}: {value}")
fruits = ["apple", "banana", "cherry"]
for index, fruit in enumerate(fruits):
print(f"{index}: {fruit}")
Tuples as Dictionary Keys
Why Lists Can't Be Keys
try:
d = {[1, 2]: "value"} # TypeError: unhashable type: 'list'
except TypeError as e:
print(f"Error: {e}")
Hashability Explained
print(hash(42)) # 42
print(hash("hello")) # some integer
print(hash((1, 2))) # some integer
# Mutable types are NOT hashable
# hash([1, 2]) # TypeError
# Tuples with mutable contents are also NOT hashable
try:
hash((1, [2, 3])) # TypeError
except TypeError as e:
print(f"Error: {e}")
# Tuples with only immutable contents ARE hashable
print(hash((1, 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
sparse = {}
sparse[(0, 0)] = 5
sparse[(2, 3)] = 10
print(sparse[(2, 3)]) # 10
# Caching with tuple keys
cache = {}
def expensive_calc(x, y):
key = (x, y)
if key not in cache:
cache[key] = x ** y
return cache[key]
print(expensive_calc(2, 10)) # 1024
print(expensive_calc(2, 10)) # 1024 (from cache)
Named Tuples
Basic Named Tuples
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(10, 20)
print(p) # Point(x=10, y=20)
print(p.x) # 10
print(p.y) # 20
print(p[0]) # 10
print(p[1]) # 20
x, y = p
print(f"x={x}, y={y}")
Named Tuples with Defaults
from collections import namedtuple
Config = namedtuple('Config', ['host', 'port', 'debug'], defaults=['localhost', 8080, False])
c1 = Config()
print(c1) # Config(host='localhost', port=8080, debug=False)
c2 = Config(host='example.com', port=9000)
print(c2) # Config(host='example.com', port=9000, debug=False)
As Lightweight Classes
from collections import namedtuple
Color = namedtuple('Color', ['red', 'green', 'blue', 'alpha'])
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
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 |
| 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
def get_min_max(numbers):
return min(numbers), max(numbers)
lo, hi = get_min_max([3, 1, 4, 1, 5, 9])
# 4. String formatting with %
print("Hello %s, you are %d years old" % ("Alice", 30))
# 5. Struct-like records
record = ("Alice", 30, "Engineer")
name, age, title = record
When to Use Lists
# 1. Dynamic collections that change
shopping_list = ["milk", "eggs"]
shopping_list.append("bread")
# 2. Data that needs sorting
scores = [85, 92, 78, 95, 88]
scores.sort()
# 3. Stacks and queues
stack = []
stack.append(1)
stack.pop()
Related Built-In Functions
len() — Number of Elements
colors = ("red", "green", "blue")
print(len(colors)) # 3
print(len(())) # 0
print(len((42,))) # 1
min() and max() — Minimum and Maximum
numbers = (4, 2, 7, 1, 9, 3)
print(min(numbers)) # 1
print(max(numbers)) # 9
# With strings
words = ("apple", "banana", "cherry")
print(min(words)) # 'apple'
print(max(words)) # 'cherry'
# With key function
students = (("Alice", 85), ("Bob", 92), ("Charlie", 78))
print(min(students, key=lambda s: s[1])) # ('Charlie', 78)
print(max(students, key=lambda s: s[1])) # ('Bob', 92)
sum() — Sum of Elements
numbers = (1, 2, 3, 4, 5)
print(sum(numbers)) # 15
# With start value
print(sum(numbers, 10)) # 25
# Sum of booleans
booleans = (True, False, True, True, False)
print(sum(booleans)) # 3
sorted() — Return New Sorted List
numbers = (3, 1, 4, 1, 5, 9, 2, 6)
print(sorted(numbers)) # [1, 1, 2, 3, 4, 5, 6, 9]
# With key
words = ("banana", "apple", "cherry")
print(sorted(words, key=len)) # ['apple', 'banana', 'cherry']
# With reverse
print(sorted(numbers, reverse=True)) # [9, 6, 5, 4, 3, 2, 1, 1]
reversed() — Return Reverse Iterator
numbers = (1, 2, 3, 4, 5)
rev = reversed(numbers)
print(list(rev)) # [5, 4, 3, 2, 1]
print(numbers) # (1, 2, 3, 4, 5) — unchanged
for num in reversed((1, 2, 3, 4, 5)):
print(num, end=" ")
# 5 4 3 2 1
enumerate() — Index and Value Pairs
colors = ("red", "green", "blue")
for index, color in enumerate(colors):
print(f"{index}: {color}")
# 0: red
# 1: green
# 2: blue
# With custom start
for index, color in enumerate(colors, start=1):
print(f"{index}. {color}")
# 1. red
# 2. green
# 3. blue
zip() — Combine Multiple Iterables
names = ("Alice", "Bob", "Charlie")
scores = (85, 92, 78)
for name, score in zip(names, scores):
print(f"{name}: {score}")
pairs = list(zip(names, scores))
print(pairs) # [('Alice', 85), ('Bob', 92), ('Charlie', 78)]
Common Use Cases
Function Return Values
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
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}")
Swapping Variables
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]
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)
Database Rows
def fetch_users():
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
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
def bbox_area(box):
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
def blend_colors(c1, c2, ratio=0.5):
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 = (42)
print(type(not_a_tuple)) # <class 'int'>
# RIGHT
is_a_tuple = (42,)
print(type(is_a_tuple)) # <class 'tuple'>
also_tuple = 42,
print(type(also_tuple)) # <class 'tuple'>
Mistake 2: Trying to Modify Tuple Elements
colors = ("red", "green", "blue")
# colors[0] = "purple" # TypeError
# Create a new tuple instead
new_colors = ("purple",) + colors[1:]
print(new_colors) # ('purple', 'green', 'blue')
Mistake 3: Confusing () with Empty Tuple vs Grouping
empty = ()
print(type(empty)) # <class 'tuple'>
grouped = (42)
print(type(grouped)) # <class 'int'>
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!
# 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
def swap_ends(t):
if len(t) <= 1:
return t
return (t[-1],) + t[1:-1] + (t[0],)
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
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})")
# #1: Diana (98)
# #2: Alice (95)
# #3: Charlie (92)
Exercise 3: Named Tuple Calculator
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)}")
# 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. - 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.