Python Full Stack Web Developer Course

Week 2 Wednesday: Tuples and When to Use Them

Introduction to Tuples

Welcome to our exploration of Python tuples! While lists often get most of the attention in Python, tuples are a foundational data structure with unique characteristics that make them invaluable in specific situations. In this lesson, we'll dive deep into tuples, understand their immutable nature, and explore the scenarios where they're the perfect tool for the job.

If lists are like a shopping cart where you can add, remove, or swap items at will, tuples are more like a sealed package with a fixed set of items – once it's packaged, the contents cannot be changed. This immutability is not a limitation but a powerful feature that brings reliability, performance benefits, and clearer intentions to your code.

Tuple Fundamentals

Creating Tuples

Tuples in Python are created using parentheses () or simply by separating values with commas:

# Creating tuples
empty_tuple = ()  # Empty tuple
single_item_tuple = (42,)  # Note the comma - essential for single-item tuples!
another_single = 42,  # Also creates a single-item tuple
coordinates = (10, 20)  # 2-item tuple
rgb_color = (255, 0, 128)  # 3-item tuple
person = ('John', 'Doe', 35, 'Developer')  # Mixed data types

# Tuple packing - the values are "packed" into a tuple
packed_tuple = 1, 2, 3, 4, 5

# Tuple unpacking - the values are "unpacked" into variables
a, b, c, d, e = packed_tuple
print(a)  # 1
print(e)  # 5

Notice the comma after the single value in (42,) – this is crucial. Without it, Python interprets (42) as just the integer 42 surrounded by parentheses used for grouping in an expression, not as a tuple.

Real-World Analogy: Sealed Product Package

A tuple is like a factory-sealed product package. Once sealed, you cannot add, remove, or replace items without breaking the seal (creating a new tuple). The integrity of the package is guaranteed – what you see is what you get, and it will stay that way.

Accessing Tuple Elements

Like lists, tuples use zero-based indexing to access elements:

coordinates = (10, 20, 30)

# Accessing elements
x = coordinates[0]  # 10
y = coordinates[1]  # 20
z = coordinates[2]  # 30

# Negative indexing
last = coordinates[-1]  # 30
second_last = coordinates[-2]  # 20

# Slicing
first_two = coordinates[:2]  # (10, 20)
last_two = coordinates[1:]   # (20, 30)

The indexing and slicing operations for tuples work exactly the same way as they do for lists. The key difference is what you can do after accessing elements.

Tuple Immutability

The defining characteristic of tuples is that they're immutable – once created, their contents cannot be modified:

point = (10, 20, 30)

# This will raise TypeError
# point[0] = 15  # TypeError: 'tuple' object does not support item assignment

# This will also raise an error
# point.append(40)  # AttributeError: 'tuple' object has no attribute 'append'

# The only way to "change" a tuple is to create a new one
new_point = (15,) + point[1:]  # (15, 20, 30)

This immutability is not a limitation but a guarantee of stability. When you pass a tuple to a function or assign it to a variable, you can be certain its contents won't be accidentally modified.

Practical Application: Geographical Coordinates

Geographic coordinates are perfect candidates for tuples. The longitude and latitude of a specific location never change – the Eiffel Tower's coordinates are fixed. Using a tuple communicates this immutability:

# Geographical coordinates for major landmarks (latitude, longitude)
eiffel_tower = (48.8584, 2.2945)
statue_of_liberty = (40.6892, -74.0445)
great_pyramid = (29.9792, 31.1342)

# Function that calculates distance between two coordinate points
def distance(point1, point2):
    """Calculate distance between two geographic points."""
    # Haversine formula calculation
    # ...
    return distance_in_km

# The immutability ensures coordinates won't be accidentally altered
user_location = (37.7749, -122.4194)  # San Francisco
print(f"Distance to Eiffel Tower: {distance(user_location, eiffel_tower):.2f} km")

Common Tuple Operations

Concatenation and Repetition

While tuples can't be modified, you can create new tuples by combining existing ones:

tuple1 = (1, 2, 3)
tuple2 = (4, 5, 6)

# Concatenation
combined = tuple1 + tuple2
print(combined)  # (1, 2, 3, 4, 5, 6)

# Repetition
repeated = tuple1 * 3
print(repeated)  # (1, 2, 3, 1, 2, 3, 1, 2, 3)

Tuple Methods

Tuples have only two built-in methods, reflecting their minimalist, immutable nature:

letters = ('a', 'b', 'c', 'a', 'd', 'a')

# count() - returns the number of occurrences of a value
count_a = letters.count('a')
print(count_a)  # 3

# index() - returns the index of the first occurrence of a value
index_b = letters.index('b')
print(index_b)  # 1

# index() with start and end parameters
index_a_after_1 = letters.index('a', 1)  # Start searching from index 1
print(index_a_after_1)  # 3

Testing Membership

You can check if a value exists in a tuple using the in operator:

colors = ('red', 'green', 'blue')

# Testing membership
has_red = 'red' in colors
print(has_red)  # True

has_yellow = 'yellow' in colors
print(has_yellow)  # False

Iterating Through Tuples

Like other sequences, tuples can be iterated through using loops:

fruits = ('apple', 'banana', 'cherry')

# Simple iteration
for fruit in fruits:
    print(fruit)

# With index using enumerate()
for index, fruit in enumerate(fruits):
    print(f"Index {index}: {fruit}")

Tuple Packing and Unpacking

One of the most powerful features of tuples is their ability to be packed and unpacked effortlessly:

# Tuple packing
coordinates = 10, 20, 30  # Creates a tuple (10, 20, 30)

# Tuple unpacking
x, y, z = coordinates
print(f"X: {x}, Y: {y}, Z: {z}")  # X: 10, Y: 20, Z: 30

# Unpacking in a for loop
points = [(1, 2), (3, 4), (5, 6)]
for x, y in points:
    print(f"X: {x}, Y: {y}")

# Extended unpacking (Python 3.x)
first, *middle, last = (1, 2, 3, 4, 5)
print(first)   # 1
print(middle)  # [2, 3, 4]
print(last)    # 5
Real-World Example: Function Return Values

Tuple unpacking shines when working with functions that return multiple values:

def get_user_stats(user_id):
    """Retrieve user statistics from database."""
    # In a real application, this would fetch data from a database
    # Here we'll just simulate it
    if user_id == 123:
        return "JohnDoe", 35, "Premium", ["Python", "JavaScript"]
    # ...

# Unpacking the returned tuple
username, age, subscription, skills = get_user_stats(123)
print(f"User: {username}, Age: {age}")
print(f"Subscription: {subscription}")
print(f"Skills: {', '.join(skills)}")

Swapping Variables

Tuple unpacking enables a clean, Pythonic way to swap variables without requiring a temporary variable:

# Traditional swap using a temporary variable
a = 5
b = 10
temp = a
a = b
b = temp
print(a, b)  # 10 5

# Pythonic swap using tuple unpacking
a = 5
b = 10
a, b = b, a  # Swap in one line!
print(a, b)  # 10 5

This elegant swapping technique leverages tuple packing and unpacking in a single expression, making the code more readable and concise.

Tuples vs. Lists: Understanding the Differences

To use tuples effectively, it's essential to understand how they differ from lists and when to choose one over the other:

Feature Tuples Lists
Mutability Immutable (cannot be changed) Mutable (can be modified)
Syntax Parentheses () or commas Square brackets []
Methods Only count() and index() Many methods (append(), insert(), sort(), etc.)
Performance Generally faster for iteration Slightly slower due to mutability overhead
Memory Usage Usually smaller memory footprint Larger memory footprint due to dynamic features
Use as Dictionary Keys Can be used as dictionary keys Cannot be used as dictionary keys
Intended Use Heterogeneous data, fixed collections Homogeneous items, changing collections
# Memory comparison example
import sys

my_list = [1, 2, 3, 4, 5]
my_tuple = (1, 2, 3, 4, 5)

list_size = sys.getsizeof(my_list)
tuple_size = sys.getsizeof(my_tuple)

print(f"List size: {list_size} bytes")
print(f"Tuple size: {tuple_size} bytes")
print(f"Difference: {list_size - tuple_size} bytes (List is {(list_size / tuple_size - 1) * 100:.2f}% larger)")
When to Use Tuples
  1. Immutable data: When data shouldn't change (e.g., days of the week, RGB color values)
  2. Dictionary keys: Use tuples when you need compound keys in dictionaries
  3. Function returns: Returning multiple values from a function
  4. Data integrity: Ensuring data can't be accidentally modified
  5. Performance-critical code: When every bit of optimization matters
When to Use Lists
  1. Dynamic collections: When items need to be added, removed, or modified
  2. Homogeneous data: Collections of the same type of items
  3. Need for in-place sorting: When data needs to be sorted or rearranged
  4. Growing collections: When the size of the collection is not known in advance

Practical Use Cases for Tuples

Named Tuples: Adding Readability

For complex tuples, Python's namedtuple from the collections module can significantly improve code readability:

from collections import namedtuple

# Creating a named tuple type
Point = namedtuple('Point', ['x', 'y', 'z'])

# Creating named tuple instances
p1 = Point(1, 2, 3)
p2 = Point(x=10, y=20, z=30)

# Accessing elements by name instead of index
print(p1.x)  # 1 (more readable than p1[0])
print(p2.y)  # 20 (more readable than p2[1])

# Named tuples are still immutable
# p1.x = 100  # This will raise AttributeError

# Named tuples can be unpacked like regular tuples
x, y, z = p2
print(x, y, z)  # 10 20 30

# They also have useful additional methods
print(p1._asdict())  # {'x': 1, 'y': 2, 'z': 3}
p3 = p1._replace(x=100)  # Creates a new named tuple with updated values
print(p3)  # Point(x=100, y=2, z=3)

Named tuples combine the immutability of tuples with the attribute access syntax of objects, giving you the best of both worlds.

Real-World Example: Database Records

Named tuples are excellent for representing database records:

from collections import namedtuple

# Define a record type
User = namedtuple('User', ['id', 'username', 'email', 'active'])

# Simulate database query results
users = [
    User(1, 'jsmith', 'john@example.com', True),
    User(2, 'ajones', 'alice@example.com', False),
    User(3, 'bwilson', 'bob@example.com', True)
]

# Processing the results is much more readable
for user in users:
    status = "active" if user.active else "inactive"
    print(f"User {user.username} ({user.email}) is {status}")
    
# Filter active users
active_users = [user for user in users if user.active]
print(f"Active users: {len(active_users)}")

Dictionary Keys

Unlike lists, tuples can be used as dictionary keys because they're immutable and hashable:

# Using tuples as dictionary keys for a sparse matrix
matrix = {}
matrix[(0, 0)] = 1
matrix[(0, 1)] = 0
matrix[(1, 0)] = 0
matrix[(1, 1)] = 1

# Accessing elements
print(matrix[(0, 0)])  # 1

# Default value for non-existent keys
print(matrix.get((2, 2), 0))  # 0

# Iterating through a sparse matrix
for (row, col), value in matrix.items():
    print(f"({row}, {col}) = {value}")

# Using tuples for multi-dimensional keys
# City, product, date -> sales
sales_data = {
    ('New York', 'Widget A', '2023-01-15'): 150,
    ('New York', 'Widget B', '2023-01-15'): 200,
    ('Los Angeles', 'Widget A', '2023-01-15'): 120,
    ('Chicago', 'Widget A', '2023-01-15'): 80
}

# We can use partial keys to create aggregation
ny_sales = sum(value for key, value in sales_data.items() if key[0] == 'New York')
widget_a_sales = sum(value for key, value in sales_data.items() if key[1] == 'Widget A')

print(f"New York total sales: {ny_sales}")
print(f"Widget A total sales: {widget_a_sales}")

Function Returns

Tuples provide a clean way to return multiple values from a function:

def get_dimensions(image_path):
    """Return the width, height, and number of channels of an image."""
    # In a real application, this would load and analyze an image
    # Here we'll just return mock values
    width = 1920
    height = 1080
    channels = 3  # RGB
    return width, height, channels  # Returns as a tuple

# Unpacking the returned values
dimensions = get_dimensions('path/to/image.jpg')
print(f"Dimensions (tuple): {dimensions}")  # (1920, 1080, 3)

# Direct unpacking
width, height, channels = get_dimensions('path/to/image.jpg')
print(f"Image is {width}x{height} with {channels} channels")

# Ignoring values we don't need
width, height, _ = get_dimensions('path/to/image.jpg')  # Ignore channels
print(f"Resolution: {width}x{height}")
Practical Application: HTTP Response Processing

When working with HTTP requests, tuples can cleanly encapsulate multiple return values:

def fetch_user_data(user_id):
    """Fetch user data from an API."""
    # In a real application, this would make an HTTP request
    # Here we simulate it
    if user_id == 123:
        # Return (status_code, headers, body)
        return (200, 
                {'Content-Type': 'application/json'}, 
                {'id': 123, 'name': 'John Doe', 'email': 'john@example.com'})
    else:
        return (404, 
                {'Content-Type': 'application/json'}, 
                {'error': 'User not found'})

# Handling the response
status, headers, body = fetch_user_data(123)

if status == 200:
    # Process successful response
    print(f"User found: {body['name']}")
    if headers['Content-Type'] == 'application/json':
        # Handle JSON data
        print(f"Email: {body['email']}")
else:
    # Handle error
    print(f"Error: {body['error']}")

Tuples in Web Development

While not as visibly common as lists, tuples play important roles in Python web development:

Database Interactions

Tuples often represent database rows in ORMs and low-level database APIs:

import sqlite3

# Connect to a database
conn = sqlite3.connect('example.db')
cursor = conn.cursor()

# Create a table
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY,
    username TEXT,
    email TEXT,
    active INTEGER
)
''')

# Insert data using tuples
users_to_insert = [
    (1, 'jsmith', 'john@example.com', 1),
    (2, 'ajones', 'alice@example.com', 0),
    (3, 'bwilson', 'bob@example.com', 1)
]

cursor.executemany('INSERT OR REPLACE INTO users VALUES (?, ?, ?, ?)', users_to_insert)
conn.commit()

# Query the database - results come back as tuples
cursor.execute('SELECT * FROM users WHERE active = 1')
active_users = cursor.fetchall()  # List of tuples

for user in active_users:
    user_id, username, email, _ = user  # Unpacking the tuple
    print(f"Active user: {username} ({email})")

conn.close()

URL Routing in Web Frameworks

Tuples are often used for URL routing configurations in web frameworks:

# Simplified example of URL patterns in a Django-like framework
url_patterns = [
    ('/', 'home_view', 'home'),
    ('/about', 'about_view', 'about'),
    ('/users', 'user_list_view', 'user_list'),
    ('/users/', 'user_detail_view', 'user_detail'),
]

# Function to match a URL path
def match_url(path):
    for pattern, view_func, name in url_patterns:
        # Simplified matching logic
        if pattern == path or (pattern.endswith('') and path.startswith(pattern[:-4])):
            return view_func
    return 'not_found_view'

# Test the router
print(match_url('/'))          # 'home_view'
print(match_url('/about'))     # 'about_view'
print(match_url('/users'))     # 'user_list_view'
print(match_url('/users/123')) # 'user_detail_view'
print(match_url('/contact'))   # 'not_found_view'

HTTP Headers

Tuples can represent HTTP headers in request and response processing:

# Simplified representation of HTTP headers using tuples
headers = [
    ('Content-Type', 'text/html; charset=utf-8'),
    ('Server', 'Python/Flask'),
    ('Cache-Control', 'no-cache'),
    ('X-Frame-Options', 'DENY')
]

# Converting to a dictionary
header_dict = dict(headers)
print(header_dict['Content-Type'])  # 'text/html; charset=utf-8'

# Adding a new header
headers.append(('X-Custom-Header', 'some-value'))

# Generating header string for HTTP response
http_headers = '\r\n'.join(f"{key}: {value}" for key, value in headers)
print(http_headers)
Real-World Example: Flask Route Registration

Flask and similar web frameworks use tuples extensively for route configuration:

# Simulating Flask-style route registration
routes = []

def route(path, methods=None):
    """Decorator to register routes."""
    if methods is None:
        methods = ('GET',)  # Default method is GET (as a tuple)
    
    def decorator(func):
        # Add route as a tuple of (path, methods, handler)
        routes.append((path, methods, func))
        return func
    
    return decorator

# Example usage
@route('/', methods=('GET', 'POST'))
def index():
    return "Home Page"

@route('/users')
def users():
    return "User List"

@route('/users/', methods=('GET', 'PUT', 'DELETE'))
def user_detail(id):
    return f"User Detail: {id}"

# Print registered routes
for path, methods, handler in routes:
    print(f"Route: {path}, Methods: {methods}, Handler: {handler.__name__}")

Performance Considerations

Tuples offer several performance advantages over lists in specific scenarios:

import timeit

# Comparing creation time
list_creation = timeit.timeit("x = [1, 2, 3, 4, 5]", number=1000000)
tuple_creation = timeit.timeit("x = (1, 2, 3, 4, 5)", number=1000000)

print(f"List creation time: {list_creation:.6f} seconds")
print(f"Tuple creation time: {tuple_creation:.6f} seconds")
print(f"Tuples are {(list_creation / tuple_creation):.2f}x faster to create\n")

# Comparing iteration time
list_iteration = timeit.timeit("for i in [1, 2, 3, 4, 5]: pass", number=1000000)
tuple_iteration = timeit.timeit("for i in (1, 2, 3, 4, 5): pass", number=1000000)

print(f"List iteration time: {list_iteration:.6f} seconds")
print(f"Tuple iteration time: {tuple_iteration:.6f} seconds")
print(f"Tuples are {(list_iteration / tuple_iteration):.2f}x faster to iterate\n")

# Comparing as dictionary keys
dict_with_tuple_keys = timeit.timeit(
    "d = {}; d[(1, 2)] = 3; x = d[(1, 2)]", number=1000000
)
# Cannot use lists as keys, so we compare with string keys
dict_with_string_keys = timeit.timeit(
    "d = {}; d['1,2'] = 3; x = d['1,2']", number=1000000
)

print(f"Dictionary with tuple keys: {dict_with_tuple_keys:.6f} seconds")
print(f"Dictionary with string keys: {dict_with_string_keys:.6f} seconds")

These performance differences might seem small in isolation, but they can add up significantly in large-scale applications, particularly in data processing or web serving with high request volumes.

When Performance Matters
  • High-volume API endpoints: Where every microsecond counts
  • Data processing pipelines: When handling millions of records
  • Cache keys: For frequently accessed composite keys
  • Memory-constrained environments: Where efficient memory usage is crucial

Limitations and Considerations

Not Always the Right Choice

Despite their advantages, tuples aren't always the best option:

When not to use tuples:

  • When items need to be added, removed, or replaced
  • When in-place sorting is required
  • When you need specialized list methods like append() or pop()
  • When the collection's size will change over time

Shallow Immutability

It's important to understand that tuple immutability is shallow – if a tuple contains mutable objects, those objects can still be modified:

# Tuple containing a list
items = (1, 2, [3, 4])

# Cannot reassign elements of the tuple
# items[0] = 10  # TypeError

# But can modify mutable elements within the tuple
items[2].append(5)  # This works!
print(items)  # (1, 2, [3, 4, 5])

# This can lead to unexpected behavior with hashing
# If a tuple contains mutable objects, it can't be used as a dictionary key
try:
    d = {}
    d[items] = 'value'
except TypeError as e:
    print(f"Error: {e}")  # Error: unhashable type: 'list'
Real-World Analogy: Sealed Box with Removable Trays

A tuple is like a sealed box that can't have new compartments added or existing ones removed. However, if one of the compartments is a removable tray (like a list), you can still add or remove items from that tray without breaking the seal on the box itself.

Practice Exercises

Exercise 1: Working with Tuples

Create functions that demonstrate tuple unpacking and multiple return values:

  1. Write a function that takes a full name as a string and returns the first name, last name, and the number of characters in the full name.
  2. Write a function that converts RGB color values to HSV (use simplified conversion for practice).
Solution
# Exercise 1: Name processing
def parse_name(full_name):
    # Split the name into parts
    parts = full_name.strip().split()
    
    # Handle edge cases
    if not parts:
        return ('', '', 0)
    elif len(parts) == 1:
        return (parts[0], '', len(parts[0]))
    
    # Standard case
    first_name = parts[0]
    last_name = parts[-1]
    total_length = len(full_name)
    
    return (first_name, last_name, total_length)

# Test cases
names = ["John Doe", "Jane Smith-Johnson", "Madonna", "  Bob  Jones  "]
for name in names:
    first, last, length = parse_name(name)
    print(f"Name: '{name}' -> First: '{first}', Last: '{last}', Length: {length}")


# Exercise 2: RGB to HSV conversion (simplified)
def rgb_to_hsv(rgb):
    r, g, b = rgb
    
    # Normalize RGB values to 0-1 range
    r, g, b = r / 255.0, g / 255.0, b / 255.0
    
    # Find max and min values
    max_val = max(r, g, b)
    min_val = min(r, g, b)
    delta = max_val - min_val
    
    # Calculate Hue (simplified)
    if delta == 0:
        h = 0  # No color, achromatic (gray)
    elif max_val == r:
        h = 60 * ((g - b) / delta % 6)
    elif max_val == g:
        h = 60 * ((b - r) / delta + 2)
    else:  # max_val == b
        h = 60 * ((r - g) / delta + 4)
    
    # Calculate Saturation
    s = 0 if max_val == 0 else delta / max_val
    
    # Value is the maximum RGB value
    v = max_val
    
    # Return HSV tuple (rounded values)
    return (round(h), round(s * 100), round(v * 100))

# Test cases
rgb_colors = [
    (255, 0, 0),    # Red
    (0, 255, 0),    # Green
    (0, 0, 255),    # Blue
    (255, 255, 0),  # Yellow
    (255, 0, 255),  # Magenta
    (0, 255, 255),  # Cyan
    (128, 128, 128) # Gray
]

for rgb in rgb_colors:
    hsv = rgb_to_hsv(rgb)
    print(f"RGB: {rgb} -> HSV: {hsv}")

Exercise 2: Named Tuples

Use named tuples to model a web application's user data:

  1. Create a named tuple called User with fields for id, username, email, name, and active status.
  2. Create several sample users.
  3. Write a function that filters active users.
  4. Write a function that formats user data as HTML table rows.
Solution
from collections import namedtuple

# Create a User named tuple
User = namedtuple('User', ['id', 'username', 'email', 'name', 'active'])

# Create sample users
users = [
    User(1, 'jdoe', 'john@example.com', 'John Doe', True),
    User(2, 'asmith', 'alice@example.com', 'Alice Smith', True),
    User(3, 'bjones', 'bob@example.com', 'Bob Jones', False),
    User(4, 'cwilson', 'charlie@example.com', 'Charlie Wilson', True),
    User(5, 'dthomas', 'david@example.com', 'David Thomas', False)
]

# Function to filter active users
def get_active_users(user_list):
    return [user for user in user_list if user.active]

# Function to format user data as HTML table rows
def users_to_html_table(user_list):
    html = ["<table>", "  <tr><th>ID</th><th>Username</th><th>Email</th><th>Name</th><th>Status</th></tr>"]
    
    for user in user_list:
        status = "Active" if user.active else "Inactive"
        row = f"  <tr><td>{user.id}</td><td>{user.username}</td><td>{user.email}</td><td>{user.name}</td><td>{status}</td></tr>"
        html.append(row)
    
    html.append("</table>")
    return "\n".join(html)

# Test the functions
active_users = get_active_users(users)
print(f"Total users: {len(users)}")
print(f"Active users: {len(active_users)}")

# Print information about active users
for user in active_users:
    print(f"- {user.name} ({user.username}): {user.email}")

# Generate HTML table
html_table = users_to_html_table(users)
print("\nHTML Table:")
print(html_table)

Exercise 3: Web Development Task

Implement a simplified router for a web application using tuples:

  1. Define route patterns as tuples with (path, HTTP method, handler function, name)
  2. Create a function that matches incoming requests to route handlers
  3. Implement simple handler functions for common routes
  4. Demonstrate routing with different types of requests
Solution
# Simple web router using tuples

# Route handler functions
def home_handler(request):
    return "Home Page"

def about_handler(request):
    return "About Us Page"

def user_list_handler(request):
    return "User List Page"

def user_detail_handler(request, user_id):
    return f"User Detail Page for user {user_id}"

def create_user_handler(request):
    return "Create User Page (POST)"

def not_found_handler(request):
    return "404 - Page Not Found"

# Define routes as tuples: (path, method, handler, name)
routes = [
    ('/', 'GET', home_handler, 'home'),
    ('/about', 'GET', about_handler, 'about'),
    ('/users', 'GET', user_list_handler, 'user_list'),
    ('/users/<id>', 'GET', user_detail_handler, 'user_detail'),
    ('/users/new', 'POST', create_user_handler, 'create_user')
]

# Router function to match request to handler
def route_request(path, method):
    # First, look for exact matches
    for route_path, route_method, handler, _ in routes:
        if path == route_path and method == route_method:
            return handler, {}
    
    # Then, look for pattern matches (<id> parameters)
    for route_path, route_method, handler, _ in routes:
        if '<id>' in route_path and method == route_method:
            base_path = route_path.replace('/<id>', '')
            if path.startswith(base_path + '/'):
                # Extract the ID from the path
                param_id = path.replace(base_path + '/', '')
                if param_id.isdigit():  # Simple validation
                    return handler, {'user_id': param_id}
    
    # Return 404 handler if no match found
    return not_found_handler, {}

# Simulate some HTTP requests
requests = [
    ('/', 'GET'),
    ('/about', 'GET'),
    ('/users', 'GET'),
    ('/users/42', 'GET'),
    ('/users/new', 'POST'),
    ('/contact', 'GET'),  # Should return 404
    ('/users', 'POST'),   # Method not allowed
    ('/users/abc', 'GET') # Invalid ID format
]

# Process requests
for path, method in requests:
    print(f"\nRequest: {method} {path}")
    handler, params = route_request(path, method)
    
    if params:
        result = handler(None, **params)
    else:
        result = handler(None)
    
    print(f"Response: {result}")

# Utility function to generate URLs from route names
def url_for(name, **params):
    for path, _, _, route_name in routes:
        if route_name == name:
            # Replace parameters in the path
            result_path = path
            for param_name, param_value in params.items():
                result_path = result_path.replace(f"<{param_name}>", str(param_value))
            return result_path
    return None

# Test URL generation
print("\nURL Generation:")
print(f"Home URL: {url_for('home')}")
print(f"User detail URL: {url_for('user_detail', id=123)}")
print(f"Non-existent route: {url_for('contact')}")

Further Topics to Explore

Exploring these topics will further enhance your understanding of tuples and their role in the Python ecosystem, particularly for web development applications.

Key Takeaways

Understanding when and why to use tuples is a mark of an experienced Python developer. They might seem like a simple variation of lists at first, but their immutability opens up unique use cases and optimization opportunities that make them indispensable in your Python toolkit.