Python & AI Tutorials Logo
Python Programming

15. Tuples and Ranges: Simple Immutable Sequences

In Chapter 14, we explored lists—Python's versatile, mutable sequence type. Now we'll examine two other important sequence types: tuples and ranges. While lists excel at storing collections that change over time, tuples provide immutable sequences that protect data from modification, and ranges offer memory-efficient ways to represent sequences of numbers.

Understanding when to use each sequence type will make your programs more efficient, safer, and clearer in intent. By the end of this chapter, you'll know how to work with tuples and ranges effectively, and you'll understand the common operations that work across all Python sequence types.

15.1) Creating and Using Tuples (The Significance of the Comma)

A tuple is an ordered, immutable sequence of items. Like lists, tuples can contain any type of data and maintain the order of elements. However, unlike lists, once you create a tuple, you cannot modify its contents.

Creating Tuples with Parentheses

The most common way to create a tuple is by enclosing comma-separated values in parentheses:

python
# A tuple of student test scores
scores = (85, 92, 78, 95)
print(scores)  # Output: (85, 92, 78, 95)
print(type(scores))  # Output: <class 'tuple'>
 
# A tuple of mixed data types
student_info = ("Alice", 20, "Computer Science", 3.8)
print(student_info)  # Output: ('Alice', 20, 'Computer Science', 3.8)
 
# An empty tuple
empty = ()
print(empty)  # Output: ()
print(len(empty))  # Output: 0

Tuples use parentheses () for their literal syntax, while lists use square brackets []. This visual distinction helps you immediately recognize which type you're working with.

The Comma Creates the Tuple, Not the Parentheses

Here's a crucial detail that surprises many beginners: the comma is what actually creates a tuple, not the parentheses. The parentheses are often optional and serve mainly to make the tuple more visible or to group it in expressions.

python
# These all create the same tuple
coordinates_1 = (10, 20)
coordinates_2 = 10, 20  # No parentheses needed!
print(coordinates_1)  # Output: (10, 20)
print(coordinates_2)  # Output: (10, 20)
print(coordinates_1 == coordinates_2)  # Output: True
 
# The comma is what matters
x = (42)  # This is just the integer 42 in parentheses
y = (42,)  # This is a tuple containing one element
print(type(x))  # Output: <class 'int'>
print(type(y))  # Output: <class 'tuple'>
print(y)  # Output: (42,)

The parentheses in (42) are just grouping parentheses, like in math expressions. To create a single-element tuple, you must include a trailing comma: (42,). This comma tells Python you want a tuple, not just a grouped expression.

When Parentheses Are Required

While the comma creates the tuple, parentheses become necessary in certain situations to avoid ambiguity:

python
# Without parentheses, this would be confusing
def get_dimensions():
    return 1920, 1080  # Returns a tuple
 
width, height = get_dimensions()
print(f"Screen: {width}x{height}")  # Output: Screen: 1920x1080
 
# Parentheses needed when passing tuples as function arguments
print((1, 2, 3))  # Output: (1, 2, 3)
# Without parentheses, Python would see three separate arguments
 
# Parentheses needed in complex expressions
result = (10, 20) + (30, 40)  # Tuple concatenation
print(result)  # Output: (10, 20, 30, 40)

Creating Single-Element Tuples

The trailing comma requirement for single-element tuples often catches beginners off guard:

python
# Common mistake: forgetting the comma
not_a_tuple = ("Python")
print(type(not_a_tuple))  # Output: <class 'str'>
print(not_a_tuple)  # Output: Python
 
# Correct: include the trailing comma
is_a_tuple = ("Python",)
print(type(is_a_tuple))  # Output: <class 'tuple'>
print(is_a_tuple)  # Output: ('Python',)
 
# The comma works even without parentheses
also_a_tuple = "Python",
print(type(also_a_tuple))  # Output: <class 'tuple'>
print(also_a_tuple)  # Output: ('Python',)

Why does Python require this seemingly awkward syntax? Because parentheses already have another meaning in Python—they group expressions. Without the comma, Python has no way to distinguish between (42) as a grouped number and (42) as a tuple.

Accessing Tuple Elements

Tuples support the same indexing and slicing operations as lists:

python
# Student information tuple
student = ("Bob", 22, "Physics", 3.6)
 
# Accessing individual elements (zero-indexed)
name = student[0]
age = student[1]
major = student[2]
gpa = student[3]
 
print(f"{name} is {age} years old")  # Output: Bob is 22 years old
print(f"Major: {major}, GPA: {gpa}")  # Output: Major: Physics, GPA: 3.6
 
# Negative indexing works too
last_item = student[-1]
print(f"Last item: {last_item}")  # Output: Last item: 3.6
 
# Slicing extracts a new tuple
first_two = student[:2]
print(first_two)  # Output: ('Bob', 22)
print(type(first_two))  # Output: <class 'tuple'>

Every indexing and slicing technique you learned with lists in Chapter 14 works identically with tuples. The key difference is that tuples cannot be modified after creation.

Multiple Items

Single Item

Empty

Tuple Creation

Syntax Choice

(item1, item2, ...)

(item,) - comma required

()

15.2) Tuple Packing and Unpacking

One of the most powerful and elegant features of tuples is their ability to pack multiple values together and unpack them into separate variables. This feature makes Python code remarkably concise and readable.

Tuple Packing

Tuple packing occurs when you create a tuple by placing multiple values together, separated by commas:

python
# Packing values into a tuple
coordinates = 10, 20, 30
print(coordinates)  # Output: (10, 20, 30)
 
# Packing different types
user_data = "Alice", 25, "alice@example.com"
print(user_data)  # Output: ('Alice', 25, 'alice@example.com')
 
# Packing function return values
def get_statistics(numbers):
    total = sum(numbers)
    count = len(numbers)
    average = total / count
    return total, count, average  # Packs three values into a tuple
 
stats = get_statistics([85, 90, 78, 92, 88])
print(stats)  # Output: (433, 5, 86.6)

When a function returns multiple values separated by commas, Python automatically packs them into a tuple. This is why functions can appear to return multiple values—they're actually returning a single tuple containing those values.

Tuple Unpacking

Tuple unpacking is the reverse process: extracting values from a tuple into separate variables:

python
# Basic unpacking
point = (100, 200)
x, y = point
print(f"x = {x}, y = {y}")  # Output: x = 100, y = 200
 
# Unpacking works with any sequence, not just tuples
name, age, email = ["Bob", 30, "bob@example.com"]
print(f"{name} is {age} years old")  # Output: Bob is 30 years old
 
# Unpacking function return values directly
total, count, average = get_statistics([95, 88, 92, 85])
print(f"Average of {count} scores: {average}")  # Output: Average of 4 scores: 90.0

The number of variables on the left side must match the number of elements in the sequence. If they don't match, Python raises a ValueError:

python
# This will cause an error
coordinates = (10, 20, 30)
# x, y = coordinates  # ValueError: too many values to unpack (expected 2)
 
# This will also cause an error
point = (5, 10)
# x, y, z = point  # ValueError: not enough values to unpack (expected 3, got 2)

Swapping Variables with Tuple Unpacking

Tuple unpacking enables an elegant way to swap variable values without needing a temporary variable:

python
# Traditional swap using a temporary variable
a = 10
b = 20
temp = a
a = b
b = temp
print(f"a = {a}, b = {b}")  # Output: a = 20, b = 10
 
# Python's elegant swap using tuple unpacking
x = 100
y = 200
x, y = y, x  # Swap in one line!
print(f"x = {x}, y = {y}")  # Output: x = 200, y = 100
 
# Swapping more than two variables
first = "A"
second = "B"
third = "C"
first, second, third = third, first, second
print(first, second, third)  # Output: C A B

How does this work? Python evaluates the right side first, creating a tuple (y, x), then unpacks it into the variables on the left side. This happens in a single step, so no temporary variable is needed.

Extended Unpacking with the Star Operator

Python provides extended unpacking using the * operator to capture multiple elements:

python
# Unpacking with a "rest" variable
scores = (95, 88, 92, 85, 90, 87)
first, second, *rest = scores
print(f"Top two: {first}, {second}")  # Output: Top two: 95, 88
print(f"Others: {rest}")  # Output: Others: [92, 85, 90, 87]
print(type(rest))  # Output: <class 'list'>
 
# The star can appear anywhere
numbers = (1, 2, 3, 4, 5)
first, *middle, last = numbers
print(f"First: {first}")  # Output: First: 1
print(f"Middle: {middle}")  # Output: Middle: [2, 3, 4]
print(f"Last: {last}")  # Output: Last: 5
 
# Capturing the beginning
*beginning, second_last, last = numbers
print(f"Beginning: {beginning}")  # Output: Beginning: [1, 2, 3]
print(f"Last two: {second_last}, {last}")  # Output: Last two: 4, 5

Notice that the starred variable always captures elements as a list, even when unpacking from a tuple. If there are no elements to capture, the starred variable becomes an empty list:

python
# When there's nothing to capture
a, b, *rest = (10, 20)
print(rest)  # Output: []
 
# Only one star allowed per unpacking
# first, *middle, *end = (1, 2, 3, 4)  # SyntaxError: multiple starred expressions

Ignoring Values with Underscore

Sometimes you only need certain values from a tuple. By convention, Python programmers use underscore _ as a variable name to indicate values they want to ignore:

python
# Parsing a date string
date_string = "2024-03-15"
year, month, day = date_string.split("-")
print(f"Month: {month}")  # Output: Month: 03
 
# If we only care about the month
_, month, _ = date_string.split("-")
print(f"Month: {month}")  # Output: Month: 03
 
# With extended unpacking
data = ("Alice", 25, "Engineer", "New York", "alice@example.com")
name, age, *_, email = data
print(f"{name} ({age}): {email}")  # Output: Alice (25): alice@example.com

The underscore is just a regular variable name, but using it signals to other programmers (and yourself) that you're intentionally ignoring those values.

Practical Examples of Packing and Unpacking

python
# Returning multiple values from calculations
def calculate_rectangle_properties(width, height):
    """Calculate area and perimeter of a rectangle."""
    area = width * height
    perimeter = 2 * (width + height)
    return area, perimeter  # Packing
 
# Unpacking the results
rect_area, rect_perimeter = calculate_rectangle_properties(5, 3)
print(f"Area: {rect_area}, Perimeter: {rect_perimeter}")  # Output: Area: 15, Perimeter: 16
 
# Iterating with unpacking
students = [
    ("Alice", 85),
    ("Bob", 92),
    ("Carol", 78)
]
 
for name, score in students:  # Unpacking in the loop
    print(f"{name}: {score}")
# Output:
# Alice: 85
# Bob: 92
# Carol: 78

Tuple packing and unpacking make Python code more readable and expressive. Instead of accessing tuple elements by index (student[0], student[1]), you can unpack them into meaningfully named variables.

15.3) Tuples Are Immutable: When That Is Useful

The defining characteristic of tuples is their immutability—once created, a tuple's contents cannot be changed. You cannot add, remove, or modify elements. This immutability might seem like a limitation, but it provides important benefits.

What Immutability Means in Practice

python
# Creating a tuple
coordinates = (10, 20, 30)
print(coordinates)  # Output: (10, 20, 30)
 
# Attempting to modify raises an error
# coordinates[0] = 15  # TypeError: 'tuple' object does not support item assignment
 
# Attempting to add elements raises an error
# coordinates.append(40)  # AttributeError: 'tuple' object has no attribute 'append'
 
# Attempting to remove elements raises an error
# del coordinates[1]  # TypeError: 'tuple' object doesn't support item deletion

When Python says tuples don't support item assignment, it means you cannot change what's stored at any position in the tuple. The tuple's structure is fixed at creation.

Comparing Mutable Lists and Immutable Tuples

python
# Lists are mutable - you can change them
shopping_list = ["milk", "bread", "eggs"]
shopping_list[1] = "butter"  # Modify an element
shopping_list.append("cheese")  # Add an element
print(shopping_list)  # Output: ['milk', 'butter', 'eggs', 'cheese']
 
# Tuples are immutable - you cannot change them
product_dimensions = (10, 20, 5)  # width, height, depth in cm
# product_dimensions[0] = 12  # TypeError: cannot modify
# product_dimensions.append(3)  # AttributeError: no append method
 
# To "change" a tuple, you must create a new one
new_dimensions = (12, 20, 5)  # Create a completely new tuple
print(new_dimensions)  # Output: (12, 20, 5)

Why Immutability Is Useful

Immutability provides several practical benefits:

1. Data Integrity and Safety

When you pass a tuple to a function, you know the function cannot accidentally modify your data:

python
def calculate_distance(point1, point2):
    """Calculate distance between two 2D points."""
    x1, y1 = point1
    x2, y2 = point2
 
    dx = x2 - x1
    dy = y2 - y1
    
    # Even if we wanted to, we can't modify the input tuples
 
    return (dx**2 + dy**2) ** 0.5
 
start = (0, 0)
end = (3, 4)
distance = calculate_distance(start, end)
print(f"Distance: {distance}")  # Output: Distance: 5.0
print(f"Start point unchanged: {start}")  # Output: Start point unchanged: (0, 0)

With lists, you'd need to worry about whether a function might modify your data. With tuples, you have a guarantee it won't.

2. Using Tuples as Dictionary Keys

As we'll explore more in Chapter 17, dictionary keys must be hashable—they must have a hash value that never changes. Immutable objects like tuples can be dictionary keys; mutable objects like lists cannot:

python
# Tuples can be dictionary keys
locations = {
    (0, 0): "Origin",
    (10, 20): "Point A",
    (30, 40): "Point B"
}
print(locations[(10, 20)])  # Output: Point A
 
# Lists cannot be dictionary keys
# locations_bad = {
#     [0, 0]: "Origin"  # TypeError: unhashable type: 'list'
# }

3. Signaling Intent

Using a tuple instead of a list communicates to other programmers (and yourself) that this data should not change:

python
# RGB color values - these should never change
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
 
# Database connection parameters - fixed configuration
DB_CONFIG = ("localhost", 5432, "myapp", "production")
 
# Geographic coordinates - a location doesn't change
EIFFEL_TOWER = (48.8584, 2.2945)  # latitude, longitude

When you see a tuple in code, you immediately know this data is meant to stay constant. When you see a list, you know it might be modified.

4. Performance Benefits

Because tuples are immutable, Python can optimize them in ways it cannot optimize lists. We'll learn about the sys module in Chapter 27, but for now, just know that sys.getsizeof() tells us how much memory an object uses:

python
import sys
 
# Tuples use less memory than equivalent lists
tuple_data = (1, 2, 3, 4, 5)
list_data = [1, 2, 3, 4, 5]
 
print(f"Tuple size: {sys.getsizeof(tuple_data)} bytes")  # Output: Tuple size: 80 bytes (may vary by Python version)
print(f"List size: {sys.getsizeof(list_data)} bytes")    # Output: List size: 104 bytes (may vary by Python version)
 
# Tuple creation is faster
import timeit
 
tuple_time = timeit.timeit("(1, 2, 3, 4, 5)", number=1000000)
list_time = timeit.timeit("[1, 2, 3, 4, 5]", number=1000000)
 
print(f"Tuple creation: {tuple_time:.4f} seconds")
print(f"List creation: {list_time:.4f} seconds")
# Example output: Tuple creation: 0.0055 seconds, List creation: 0.0292 seconds

15.4) The Immutability Trap: When Tuples Contain Mutable Items

While tuples themselves are immutable, they can contain mutable objects like lists or dictionaries. This creates a subtle but important distinction: the tuple's structure is fixed, but the contents of mutable objects inside it can still change.

Understanding the Distinction

python
# A tuple containing a list
student_data = ("Alice", 20, [85, 90, 78])  # name, age, scores
print(student_data)  # Output: ('Alice', 20, [85, 90, 78])
 
# We cannot reassign tuple elements
# student_data[0] = "Bob"  # TypeError: 'tuple' object does not support item assignment
 
# But we CAN modify the list inside the tuple
student_data[2].append(92)  # Add a new score
print(student_data)  # Output: ('Alice', 20, [85, 90, 78, 92])
 
student_data[2][0] = 88  # Modify an existing score
print(student_data)  # Output: ('Alice', 20, [88, 90, 78, 92])

What's happening here? The tuple stores three references: one to the string "Alice", one to the integer 20, and one to a list object. The tuple's structure—which objects it references—cannot change. But the list object itself is mutable, so its contents can change.

Visualizing the Difference

python
# The tuple structure is fixed
data = ("Python", [1, 2, 3])
 
# This tries to change what the tuple references - NOT ALLOWED
# data[1] = [4, 5, 6]  # TypeError
 
# This modifies the list that the tuple references - ALLOWED
data[1].append(4)
print(data)  # Output: ('Python', [1, 2, 3, 4])
 
# The tuple still references the same list object
# Only the list's contents changed, not which list the tuple points to

Think of it this way: a tuple is like a row of boxes, and each box contains a reference to an object. The boxes themselves are locked in place (immutable), but if a box contains a reference to a mutable object, that object can still change.

Tuples with Dictionaries

The same principle applies to dictionaries inside tuples:

python
# Tuple containing a dictionary
user_profile = ("alice", {"email": "alice@example.com", "age": 25})
print(user_profile)  # Output: ('alice', {'email': 'alice@example.com', 'age': 25})
 
# Cannot change which dictionary the tuple references
# user_profile[1] = {"email": "newemail@example.com"}  # TypeError
 
# But CAN modify the dictionary itself
user_profile[1]["age"] = 26
user_profile[1]["city"] = "New York"
print(user_profile)  # Output: ('alice', {'email': 'alice@example.com', 'age': 26, 'city': 'New York'})

Why This Matters for Dictionary Keys

Tuples can be used as dictionary keys only if all of their elements are hashable. Although tuples themselves are immutable, a tuple that contains mutable objects (like lists) is not hashable at all and therefore cannot be used as a dictionary key.

python
# This works but is dangerous
tuple_with_list = ("key", [1, 2, 3])
# data = {tuple_with_list: "value"}  # TypeError: unhashable type: 'list'

Only use tuples containing fully immutable objects (strings, numbers, frozensets, other tuples) as dictionary keys.

Creating Truly Immutable Tuples

If you need a tuple that's completely immutable, ensure all its contents are also immutable:

python
# Fully immutable tuple - only immutable types
point_3d = (10, 20, 30)  # All integers
rgb_color = (255, 128, 0)  # All integers
coordinates = ((10, 20), (30, 40))  # Tuple of tuples
 
# These are safe to use as dictionary keys
color_names = {
    (255, 0, 0): "Red",
    (0, 255, 0): "Green",
    (0, 0, 255): "Blue"
}
 
# Nested tuples remain immutable
nested = ((1, 2), (3, 4))
# nested[0][0] = 5  # TypeError: 'tuple' object does not support item assignment

When Mutable Contents Are Intentional

Sometimes you actually want a tuple with mutable contents—for example, when you have a fixed record structure but one field needs to change:

python
# Student record with fixed identity but changing grades
def create_student(name, student_id):
    """Create a student record with empty grade list."""
    return (name, student_id, [])  # name and ID fixed, grades can change
 
student = create_student("Alice", "S12345")
print(student)  # Output: ('Alice', 'S12345', [])
 
# The student's identity is fixed
print(f"Student: {student[0]} (ID: {student[1]})")  # Output: Student: Alice (ID: S12345)
 
# But we can add grades as they're earned
student[2].append(85)
student[2].append(92)
student[2].append(78)
print(f"Grades: {student[2]}")  # Output: Grades: [85, 92, 78]
 
# The tuple structure protects name and ID from accidental changes
# while allowing the grade list to grow

This pattern is useful when you want to protect some data while allowing other data to change. Just be aware of the distinction between the tuple's immutability and its contents' mutability.

15.5) When to Use Tuples Instead of Lists

Choosing between tuples and lists is an important design decision. While they're both sequences, they serve different purposes and communicate different intentions.

Use Tuples for Fixed, Heterogeneous Data

Tuples work best when you have a fixed number of items that represent a single logical entity, often with different types:

python
# Student record: name, age, major, GPA
student = ("Alice", 20, "Computer Science", 3.8)
 
# Geographic coordinates: latitude, longitude
location = (40.7128, -74.0060)  # New York City
 
# RGB color: red, green, blue
color = (255, 128, 0)
 
# Database connection: host, port, database, username
db_connection = ("localhost", 5432, "myapp", "admin")
 
# Date: year, month, day
date = (2024, 3, 15)

Each tuple represents a complete "record" where the position of each element has a specific meaning. The first element is always the name, the second is always the age, and so on.

Use Lists for Homogeneous Collections

Lists work best when you have a variable number of similar items that you might add to, remove from, or reorder:

python
# Shopping list - items of the same type (strings)
shopping_list = ["milk", "bread", "eggs", "butter"]
shopping_list.append("cheese")  # Add more items as needed
shopping_list.remove("bread")   # Remove items
 
# Test scores - items of the same type (numbers)
test_scores = [85, 92, 78, 95, 88]
test_scores.append(90)  # Add new score
test_scores.sort()      # Reorder scores
 
# User names - items of the same type (strings)
active_users = ["alice", "bob", "carol"]
active_users.extend(["dave", "eve"])  # Add multiple users

Lists are for collections where the number of items might change and where each item plays the same role.

Tuples for Function Return Values

When a function returns multiple related values, tuples are the natural choice:

python
def get_user_info(user_id):
    """Retrieve user information from database."""
    # Simulate database lookup
    return "Alice", "alice@example.com", 25, "New York"
 
# Unpack the returned tuple
name, email, age, city = get_user_info(101)
print(f"{name} from {city}")  # Output: Alice from New York
 
def calculate_statistics(numbers):
    """Calculate min, max, and average of numbers."""
    if not numbers:
        return None, None, None
    
    minimum = min(numbers)
    maximum = max(numbers)
    average = sum(numbers) / len(numbers)
    return minimum, maximum, average
 
# Unpack the results
min_val, max_val, avg_val = calculate_statistics([85, 92, 78, 95, 88])
print(f"Range: {min_val} to {max_val}, Average: {avg_val}")
# Output: Range: 78 to 95, Average: 87.6

Returning tuples makes it clear that these values are related and should be considered together.

Tuples for Dictionary Keys

When you need composite keys in a dictionary, tuples are essential:

python
# Student grades by course and semester
grades = {
    ("CS101", "Fall2023"): 85,
    ("CS101", "Spring2024"): 90,
    ("MATH201", "Fall2023"): 88,
    ("MATH201", "Spring2024"): 92
}
 
# Look up a specific grade
course = "CS101"
semester = "Spring2024"
grade = grades[(course, semester)]
print(f"Grade in {course} ({semester}): {grade}")  # Output: Grade in CS101 (Spring2024): 90
 
# Grid coordinates as dictionary keys
grid = {
    (0, 0): "Start",
    (5, 3): "Obstacle",
    (10, 10): "Goal"
}
 
position = (5, 3)
if position in grid:
    print(f"At {position}: {grid[position]}")  # Output: At (5, 3): Obstacle

Lists cannot be dictionary keys because they're mutable, but tuples can.

Tuples for Immutable Configuration

When you have configuration data that should never change, tuples signal this intent:

python
# Application settings that should remain constant
APP_CONFIG = (
    "MyApp",           # Application name
    "1.0.0",          # Version
    "production",     # Environment
    True,             # Debug mode
    8080              # Port
)
 
# Color palette for UI - these colors are fixed
COLOR_PALETTE = (
    (255, 0, 0),      # Primary red
    (0, 128, 255),    # Primary blue
    (255, 255, 255),  # White
    (0, 0, 0)         # Black
)
 
# API endpoints - these URLs don't change
API_ENDPOINTS = (
    "https://api.example.com/users",
    "https://api.example.com/products",
    "https://api.example.com/orders"
)

Decision Guide

python
# Use TUPLES when:
# 1. Data represents a single record with fixed structure
employee = ("E001", "Alice", "Engineering", 75000)
 
# 2. Returning multiple values from a function
def divide_with_remainder(a, b):
    return a // b, a % b
 
# 3. Need to use as dictionary keys
cache = {(5, 10): 50, (3, 7): 21}
 
# 4. Data should not be modified
SCREEN_RESOLUTION = (1920, 1080)
 
# Use LISTS when:
# 1. Collection of similar items that might change
tasks = ["Write code", "Test code", "Deploy code"]
tasks.append("Document code")
 
# 2. Need to add, remove, or reorder items
scores = [85, 90, 78]
scores.sort()
scores.append(92)
 
# 3. All items serve the same purpose
usernames = ["alice", "bob", "carol"]
 
# 4. Size of collection is not known in advance
results = []
for i in range(10):
    results.append(i * 2)

15.6) Understanding range Objects in Depth

Now that we understand when to use tuples versus lists, let's explore Python's third immutable sequence type: ranges. The range type represents an immutable sequence of numbers. Unlike lists and tuples that store all their elements in memory, range objects generate numbers on demand, making them extremely memory-efficient for representing large sequences.

Creating range Objects

The range() function creates range objects with three forms:

python
# Single argument: range(stop)
# Generates numbers from 0 up to (but not including) stop
numbers = range(5)
print(list(numbers))  # Output: [0, 1, 2, 3, 4]
 
# Two arguments: range(start, stop)
# Generates numbers from start up to (but not including) stop
numbers = range(2, 7)
print(list(numbers))  # Output: [2, 3, 4, 5, 6]
 
# Three arguments: range(start, stop, step)
# Generates numbers from start up to stop, incrementing by step
numbers = range(0, 10, 2)
print(list(numbers))  # Output: [0, 2, 4, 6, 8]
 
# Negative step for counting down
numbers = range(10, 0, -1)
print(list(numbers))  # Output: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

Note that we convert ranges to lists with list() to see their contents. A range object itself doesn't display all its values when printed:

python
r = range(5)
print(r)  # Output: range(0, 5)
print(type(r))  # Output: <class 'range'>

How range Objects Work

Range objects don't store all their values in memory. Instead, they calculate each value when needed:

python
import sys
 
# A range representing a million numbers
large_range = range(1000000)
print(f"Range size: {sys.getsizeof(large_range)} bytes")  # Output: Range size: 48 bytes (may vary by Python version)
 
# A list containing a million numbers
large_list = list(range(1000000))
print(f"List size: {sys.getsizeof(large_list)} bytes")  # Output: List size: 8000056 bytes (approximately 8MB)
 
# The range is tiny; the list is huge!

A range object only stores three values: start, stop, and step. It calculates each number in the sequence when you ask for it. This makes ranges incredibly efficient for large sequences.

Using range in for Loops

As we learned in Chapter 12, ranges are most commonly used with for loops:

python
# Counting from 0 to 4
for i in range(5):
    print(f"Count: {i}")
# Output:
# Count: 0
# Count: 1
# Count: 2
# Count: 3
# Count: 4
 
# Counting from 1 to 10
for i in range(1, 11):
    print(i, end=" ")
print()  # Output: 1 2 3 4 5 6 7 8 9 10
 
# Counting by twos
for i in range(0, 20, 2):
    print(i, end=" ")
print()  # Output: 0 2 4 6 8 10 12 14 16 18
 
# Counting backwards
for i in range(5, 0, -1):
    print(f"T-minus {i}")
# Output:
# T-minus 5
# T-minus 4
# T-minus 3
# T-minus 2
# T-minus 1

Indexing and Slicing range Objects

Range objects support indexing and slicing just like other sequences:

python
# Creating a range
numbers = range(10, 50, 5)  # 10, 15, 20, 25, 30, 35, 40, 45
 
# Indexing
print(numbers[0])   # Output: 10
print(numbers[3])   # Output: 25
print(numbers[-1])  # Output: 45
 
# Slicing returns a new range
subset = numbers[2:5]
print(subset)  # Output: range(20, 35, 5)
print(list(subset))  # Output: [20, 25, 30]
 
# Length
print(len(numbers))  # Output: 8

Checking Membership

You can check if a number is in a range using the in operator:

python
# Even numbers from 0 to 20
evens = range(0, 21, 2)
 
print(10 in evens)  # Output: True
print(15 in evens)  # Output: False
print(20 in evens)  # Output: True
 
# This is very efficient - Python doesn't generate all numbers
# It calculates whether the number would be in the sequence
large_range = range(0, 1000000, 3)
print(999999 in large_range)  # Output: True (instant, no iteration needed)

Python can determine membership mathematically without generating all the numbers, making this operation extremely fast even for huge ranges.

Empty and Reverse Ranges

python
# Empty range - stop equals start
empty = range(5, 5)
print(list(empty))  # Output: []
print(len(empty))   # Output: 0
 
# Empty range - impossible to reach stop with given step
impossible = range(1, 10, -1)  # Can't count up with negative step
print(list(impossible))  # Output: []
 
# Reverse range
backwards = range(10, 0, -1)
print(list(backwards))  # Output: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
 
# Reverse with negative numbers
negative_range = range(-5, -15, -2)
print(list(negative_range))  # Output: [-5, -7, -9, -11, -13]

When to Use range vs Lists

python
# Use range when:
# 1. You need a sequence of numbers for iteration
for i in range(100):
    # Process something 100 times
    pass
 
# 2. You need indices for a sequence
items = ["a", "b", "c", "d"]
for i in range(len(items)):
    print(f"Index {i}: {items[i]}")
 
# 3. Memory efficiency matters with large sequences
# This uses minimal memory
for i in range(1000000):
    if i % 100000 == 0:
        print(i)
 
# Use lists when:
# 1. You need to store the actual values
squares = [1, 3, 5, 7, 10]
 
# 2. You need to modify the sequence
numbers = list(range(5))
numbers[2] = 100  # Modify a value
numbers.append(200)  # Add a value
 
# 3. You need to use the sequence multiple times with different operations
data = list(range(10))
print(sum(data))
print(max(data))
print(sorted(data, reverse=True))

Range objects are a perfect example of Python's efficiency. They provide all the benefits of a sequence without the memory cost of storing every element.

15.7) Converting Between Lists, Tuples, and Ranges

Python makes it easy to convert between different sequence types. Understanding these conversions helps you choose the right type for each situation and transform data as needed.

Converting to Lists

The list() function converts any sequence to a list:

python
# Tuple to list
student_tuple = ("Alice", 20, "CS")
student_list = list(student_tuple)
print(student_list)  # Output: ['Alice', 20, 'CS']
print(type(student_list))  # Output: <class 'list'>
 
# Now we can modify it
student_list[1] = 21
student_list.append(3.8)
print(student_list)  # Output: ['Alice', 21, 'CS', 3.8]
 
# Range to list
numbers = range(5)
numbers_list = list(numbers)
print(numbers_list)  # Output: [0, 1, 2, 3, 4]
 
# String to list (each character becomes an element)
text = "Python"
chars = list(text)
print(chars)  # Output: ['P', 'y', 't', 'h', 'o', 'n']

Converting to a list is useful when you need to modify a sequence or when you need to use list-specific methods like append(), sort(), or remove().

Converting to Tuples

The tuple() function converts any sequence to a tuple:

python
# List to tuple
scores_list = [85, 90, 78, 92]
scores_tuple = tuple(scores_list)
print(scores_tuple)  # Output: (85, 90, 78, 92)
print(type(scores_tuple))  # Output: <class 'tuple'>
 
# Now it's immutable
# scores_tuple[0] = 88  # TypeError: 'tuple' object does not support item assignment
 
# Range to tuple
numbers = range(1, 6)
numbers_tuple = tuple(numbers)
print(numbers_tuple)  # Output: (1, 2, 3, 4, 5)
 
# String to tuple
text = "Hi"
chars_tuple = tuple(text)
print(chars_tuple)  # Output: ('H', 'i')

Converting to a tuple is useful when you want to protect data from modification or when you need to use a sequence as a dictionary key.

list

tuple

tuple

list

Range

List

Tuple

15.8) Common Sequence Operations Across Strings, Lists, Tuples, and Ranges

Python's sequence types—strings, lists, tuples, and ranges—share many common operations. Understanding these shared operations helps you work efficiently with any sequence type.

Length, Minimum, and Maximum

All sequences support the len(), min(), and max() functions:

python
# Strings
text = "Python"
print(len(text))  # Output: 6
print(min(text))  # Output: P (smallest character by Unicode value)
print(max(text))  # Output: y (largest character by Unicode value)
 
# Lists
numbers = [45, 12, 78, 23, 56]
print(len(numbers))  # Output: 5
print(min(numbers))  # Output: 12
print(max(numbers))  # Output: 78
 
# Tuples
scores = (85, 92, 78, 95, 88)
print(len(scores))  # Output: 5
print(min(scores))  # Output: 78
print(max(scores))  # Output: 95
 
# Ranges
nums = range(10, 50, 5)
print(len(nums))  # Output: 8
print(min(nums))  # Output: 10
print(max(nums))  # Output: 45

For min() and max() to work, the elements must be comparable. You can't find the minimum of a list containing both strings and numbers:

python
mixed = [1, "hello", 3]
# print(min(mixed))  # TypeError: '<' not supported between instances of 'str' and 'int'

Indexing and Negative Indexing

All sequences support indexing with positive and negative indices:

python
# Positive indexing (0-based)
text = "Python"
numbers = [10, 20, 30, 40, 50]
coords = (5, 10, 15)
values = range(0, 100, 10)
 
print(text[0])      # Output: P
print(numbers[2])   # Output: 30
print(coords[1])    # Output: 10
print(values[3])    # Output: 30
 
# Negative indexing (from the end)
print(text[-1])     # Output: n (last character)
print(numbers[-2])  # Output: 40 (second from end)
print(coords[-3])   # Output: 5 (third from end, which is first)
print(values[-1])   # Output: 90 (last value in range)

Negative indices count from the end: -1 is the last element, -2 is second-to-last, and so on.

Membership Testing with in and not in

All sequences support membership testing:

python
# Strings - checks for substrings
text = "Python Programming"
print("Python" in text)      # Output: True
print("Java" in text)        # Output: False
print("gram" in text)        # Output: True (substring)
print("PYTHON" not in text)  # Output: True (case-sensitive)
 
# Lists
fruits = ["apple", "banana", "cherry", "date"]
print("banana" in fruits)    # Output: True
print("grape" in fruits)     # Output: False
print("apple" not in fruits) # Output: False
 
# Tuples
coordinates = (10, 20, 30, 40)
print(20 in coordinates)     # Output: True
print(25 in coordinates)     # Output: False
print(50 not in coordinates) # Output: True
 
# Ranges - very efficient, no iteration needed
numbers = range(0, 100, 2)  # Even numbers 0 to 98
print(50 in numbers)         # Output: True
print(51 in numbers)         # Output: False (odd number)
print(100 in numbers)        # Output: False (stop is exclusive)

For ranges, Python can determine membership mathematically without checking every element, making it extremely fast even for huge ranges.

Concatenation and Repetition

Strings, lists, and tuples support concatenation with + and repetition with *:

python
# Concatenation with +
text1 = "Hello"
text2 = " World"
print(text1 + text2)  # Output: Hello World
 
list1 = [1, 2, 3]
list2 = [4, 5, 6]
print(list1 + list2)  # Output: [1, 2, 3, 4, 5, 6]
 
tuple1 = (10, 20)
tuple2 = (30, 40)
print(tuple1 + tuple2)  # Output: (10, 20, 30, 40)
 
# Repetition with *
print("Ha" * 3)           # Output: HaHaHa
print([0] * 5)            # Output: [0, 0, 0, 0, 0]
print((1, 2) * 3)         # Output: (1, 2, 1, 2, 1, 2)

Important: Ranges do not support concatenation or repetition:

python
r1 = range(5)
r2 = range(5, 10)
# combined = r1 + r2  # TypeError: unsupported operand type(s) for +: 'range' and 'range'
 
# To combine ranges, convert to lists or tuples first
combined = list(r1) + list(r2)
print(combined)  # Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Counting Occurrences

The count() method returns how many times an element appears:

python
# Strings - counts substring occurrences
text = "Mississippi"
print(text.count("s"))   # Output: 4
print(text.count("ss"))  # Output: 2
print(text.count("i"))   # Output: 4
 
# Lists
numbers = [1, 2, 3, 2, 4, 2, 5]
print(numbers.count(2))  # Output: 3
print(numbers.count(6))  # Output: 0
 
# Tuples
grades = (85, 90, 85, 92, 85, 88)
print(grades.count(85))  # Output: 3
print(grades.count(95))  # Output: 0
 
# Ranges don't have count() method, but you can convert first
nums = range(0, 20, 2)
nums_list = list(nums)
print(nums_list.count(10))  # Output: 1

Finding Index of Elements

The index() method returns the position of the first occurrence:

python
# Strings
text = "Python Programming"
print(text.index("P"))      # Output: 0 (first P)
print(text.index("Pro"))    # Output: 7 (substring position)
# print(text.index("Java"))  # ValueError: substring not found
 
# Lists
fruits = ["apple", "banana", "cherry", "banana"]
print(fruits.index("banana"))  # Output: 1 (first occurrence)
print(fruits.index("cherry"))  # Output: 2
# print(fruits.index("grape"))  # ValueError: 'grape' is not in list
 
# Tuples
coordinates = (10, 20, 30, 20, 40)
print(coordinates.index(20))  # Output: 1 (first occurrence)
print(coordinates.index(40))  # Output: 4
 
# Ranges don't have index() method, but you can convert first
nums = range(10, 50, 5)
nums_list = list(nums)
print(nums_list.index(25))  # Output: 3

If the element isn't found, index() raises a ValueError. To avoid this, check with in first:

python
fruits = ["apple", "banana", "cherry"]
search_fruit = "grape"
 
if search_fruit in fruits:
    position = fruits.index(search_fruit)
    print(f"{search_fruit} found at position {position}")
else:
    print(f"{search_fruit} not found")
# Output: grape not found

Iteration with for Loops

All sequences can be iterated with for loops:

python
# Strings - iterate over characters
for char in "Python":
    print(char, end=" ")
print()  # Output: P y t h o n
 
# Lists
for fruit in ["apple", "banana", "cherry"]:
    print(f"I like {fruit}")
# Output:
# I like apple
# I like banana
# I like cherry
 
# Tuples
for score in (85, 90, 78):
    print(f"Score: {score}")
# Output:
# Score: 85
# Score: 90
# Score: 78
 
# Ranges
for i in range(1, 6):
    print(f"Count: {i}")
# Output:
# Count: 1
# Count: 2
# Count: 3
# Count: 4
# Count: 5

Comparison Operations

Sequences can be compared using ==, !=, <, >, <=, and >=:

python
# Equality
print([1, 2, 3] == [1, 2, 3])      # Output: True
print((1, 2, 3) == (1, 2, 3))      # Output: True
print("abc" == "abc")               # Output: True
 
# Inequality
print([1, 2, 3] != [1, 2, 4])      # Output: True
print((1, 2) != (1, 2))            # Output: False
 
# Lexicographic comparison (element by element)
print([1, 2, 3] < [1, 2, 4])       # Output: True (3 < 4)
print([1, 2, 3] < [1, 3, 0])       # Output: True (2 < 3)
print("apple" < "banana")           # Output: True (alphabetical)
print((1, 2) < (1, 2, 3))          # Output: True (shorter is less if equal so far)
 
# Comparing different types
print([1, 2, 3] == (1, 2, 3))      # Output: False (different types)

Comparison works element by element from left to right. The first differing element determines the result.

Understanding these common operations allows you to write code that works with any sequence type, making your programs more flexible and reusable.

15.9) Advanced Slicing Across All Sequence Types

Slicing is one of Python's most powerful features for working with sequences. While we introduced basic slicing in Chapter 14, there are advanced slicing techniques that work across all sequence types.

Basic Slicing Review

Slicing extracts a portion of a sequence using the syntax sequence[start:stop:step]:

python
# Basic slicing with strings
text = "Python Programming"
print(text[0:6])    # Output: Python
print(text[7:18])   # Output: Programming
print(text[7:])     # Output: Programming (from index 7 to end)
print(text[:6])     # Output: Python (from start to index 6)
 
# Basic slicing with lists
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[2:7])   # Output: [2, 3, 4, 5, 6]
print(numbers[:5])    # Output: [0, 1, 2, 3, 4]
print(numbers[5:])    # Output: [5, 6, 7, 8, 9]
 
# Basic slicing with tuples
coordinates = (10, 20, 30, 40, 50, 60)
print(coordinates[1:4])  # Output: (20, 30, 40)
print(coordinates[:3])   # Output: (10, 20, 30)
print(coordinates[3:])   # Output: (40, 50, 60)
 
# Basic slicing with ranges
nums = range(0, 100, 10)
print(list(nums[2:5]))   # Output: [20, 30, 40]

Remember: start is inclusive, stop is exclusive, and the result is always the same type as the original sequence.

Using Step in Slicing

The optional third parameter step controls how many elements to skip:

python
# Every second element
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[::2])     # Output: [0, 2, 4, 6, 8]
print(numbers[1::2])    # Output: [1, 3, 5, 7, 9]
 
# Every third element
text = "abcdefghijklmnop"
print(text[::3])        # Output: adgjmp
 
# Step with start and stop
print(numbers[2:8:2])   # Output: [2, 4, 6]
print(text[1:10:2])     # Output: bdfhj

Negative Step: Reversing Sequences

A negative step reverses the direction of slicing:

python
# Reversing entire sequences
text = "Python"
print(text[::-1])       # Output: nohtyP
 
numbers = [1, 2, 3, 4, 5]
print(numbers[::-1])    # Output: [5, 4, 3, 2, 1]
 
coordinates = (10, 20, 30, 40)
print(coordinates[::-1])  # Output: (40, 30, 20, 10)
 
# Reversing with step
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[::-2])    # Output: [9, 7, 5, 3, 1] (every second, backwards)
 
# Reversing a portion
text = "Python Programming"
print(text[7:18][::-1])  # Output: gnimmargorP (reverse "Programming")

When using negative step, start and stop work differently:

python
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
 
# With negative step, start should be greater than stop
print(numbers[7:2:-1])   # Output: [7, 6, 5, 4, 3] (from 7 down to 3)
print(numbers[8:3:-2])   # Output: [8, 6, 4] (from 8 down to 4, step -2)
 
# Omitting start/stop with negative step
print(numbers[:5:-1])    # Output: [9, 8, 7, 6] (from end down to 6)
print(numbers[5::-1])    # Output: [5, 4, 3, 2, 1, 0] (from 5 down to start)

Negative Indices in Slicing

You can use negative indices for start and stop positions:

python
text = "Python Programming"
# Last 11 characters
print(text[-11:])        # Output: Programming
 
# All but last 11 characters
print(text[:-11])        # Output: Python
 
# From -15 to -5
print(text[-15:-5])      # Output: hon Progra
 
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# Last 5 elements
print(numbers[-5:])      # Output: [5, 6, 7, 8, 9]
 
# All but last 3 elements
print(numbers[:-3])      # Output: [0, 1, 2, 3, 4, 5, 6]
 
# From -7 to -2
print(numbers[-7:-2])    # Output: [3, 4, 5, 6, 7]

Slicing with Ranges

Slicing a range returns a new range object:

python
# Slicing ranges
numbers = range(0, 100, 5)  # 0, 5, 10, 15, ..., 95
print(numbers)  # Output: range(0, 100, 5)
 
# Slice returns a new range
subset = numbers[5:10]
print(subset)  # Output: range(25, 50, 5)
print(list(subset))  # Output: [25, 30, 35, 40, 45]
 
# With step
every_other = numbers[::2]
print(list(every_other))  # Output: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
 
# Negative step
reversed_range = numbers[::-1]
print(list(reversed_range))  # Output: [95, 90, 85, ..., 5, 0]

Empty Slices and Edge Cases

python
numbers = [1, 2, 3, 4, 5]
 
# Empty slices (start >= stop with positive step)
print(numbers[3:3])    # Output: []
print(numbers[5:10])   # Output: [] (stop beyond length)
print(numbers[10:20])  # Output: [] (both beyond length)
 
# Slices beyond sequence bounds are safe
print(numbers[-100:100])  # Output: [1, 2, 3, 4, 5] (entire sequence)
print(numbers[2:100])     # Output: [3, 4, 5] (from 2 to end)
 
# Negative step with incompatible start/stop
print(numbers[2:7:-1])    # Output: [] (can't go forward with negative step)
 
# Step of 0 is not allowed
# print(numbers[::0])  # ValueError: slice step cannot be zero

Slicing for Copying

Slicing creates a new sequence, which provides a way to copy:

python
# Copying with slicing
original = [1, 2, 3, 4, 5]
copy = original[:]  # Slice from start to end
print(copy)  # Output: [1, 2, 3, 4, 5]
 
# Modifying the copy doesn't affect the original
copy[0] = 100
print(f"Original: {original}")  # Output: Original: [1, 2, 3, 4, 5]
print(f"Copy: {copy}")          # Output: Copy: [100, 2, 3, 4, 5]
 
# This works for tuples too (creates a new tuple)
original_tuple = (1, 2, 3, 4, 5)
copy_tuple = original_tuple[:]
print(copy_tuple)  # Output: (1, 2, 3, 4, 5)
 
# For strings
text = "Python"
text_copy = text[:]
print(text_copy)  # Output: Python

However, remember from Chapter 14 that this creates a shallow copy.

python
# Shallow copy limitation
original = [[1, 2], [3, 4]]
copy = original[:]
 
# Modifying a nested list affects both
copy[0][0] = 100
print(f"Original: {original}")  # Output: Original: [[100, 2], [3, 4]]
print(f"Copy: {copy}")          # Output: Copy: [[100, 2], [3, 4]]

Tuples and ranges are essential tools in Python's sequence toolkit. Tuples provide immutable, structured data that protects information from accidental modification and enables their use as dictionary keys. Ranges offer memory-efficient representations of number sequences, perfect for loops and large sequences. Understanding when to use each type—and how to convert between them—makes your code more efficient, safer, and clearer in intent.

The common operations shared across all sequence types—indexing, slicing, iteration, membership testing—form a consistent interface that makes working with any sequence intuitive. Advanced slicing techniques give you powerful, expressive ways to extract and manipulate sequence data.

As you continue programming in Python, you'll find yourself naturally choosing the right sequence type for each situation: lists for collections that change, tuples for fixed records, ranges for number sequences, and strings for text. This chapter has given you the knowledge to make those choices confidently and use each type effectively.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai