Skip to content

When to Use Python Tuples Over Lists: The Immutability Advantage

Problem

When I first learned Python, I kept using lists everywhere. They’re flexible, you can modify them, and they work fine for most cases. Then I saw experienced developers using tuples, and I wondered: why bother with tuples when lists can do everything tuples can do, plus more?

I asked myself: “What’s the point of a data structure that can’t be modified?”

I think the confusion comes from thinking about tuples as “immutable lists” rather than understanding what they’re actually designed for.

What I discovered

I tried to understand the practical differences by using both in my code. Here’s what I found:

# List: mutable, can change
coordinates = [10, 20]
coordinates[0] = 99 # Works, but is this what I want?
# Tuple: immutable, prevents accidental changes
coordinates = (10, 20)
coordinates[0] = 99 # TypeError: 'tuple' object does not support item assignment

At first, I thought the tuple version was just being restrictive. But then I encountered a real bug in production.

The bug that changed my thinking

I was working on a function with a default argument:

buggy.py
def add_item(item, items=[]):
items.append(item)
return items

When I called this function multiple times:

print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] - Wait, where did the 1 come from?

The list persisted across function calls. This is Python’s mutable default argument gotcha—the list is created once when the function is defined, not each time it’s called.

I tried fixing it with a tuple:

fixed.py
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items

Now it works correctly:

print(add_item(1)) # [1]
print(add_item(2)) # [2]

But I realized the deeper issue: I was using a mutable structure (list) for something that should have been immutable from the start.

Why tuple immutability matters

After dealing with this bug and working on larger codebases, I found several practical reasons to use tuples:

1. Enforcing “this should never change”

In large codebases with multiple developers, data gets passed around through different functions. When you use a list, anyone can modify it:

# In some function far away
def process_location(coords):
coords[0] = 0 # Someone accidentally modified this
# Your original data
coordinates = [10, 20]
process_location(coordinates)
# Now coordinates is [0, 20] - silent bug!

With a tuple, this can’t happen:

coordinates = (10, 20)
process_location(coordinates)
# TypeError prevents the bug

I think the key insight is that tuples enforce constraints at the language level. In a 50,000-line codebase, you can’t track who might modify data. Immutability moves that burden from “developer discipline” to “language guarantees.”

2. Semantic meaning: position matters

I realized tuples represent records where each position has specific meaning, while lists represent collections of similar items:

# Tuple: position has meaning
# Format: (red, green, blue)
color = (255, 128, 0)
# List: collection of similar items
shades = [128, 150, 200, 255]

When I see color[0], I know it’s the red value. When I see shades[0], it’s just the first item in a collection.

This distinction becomes clearer with type hints:

from typing import Tuple
# Each position has a specific type
user_info: Tuple[str, int, bool] = ("Alice", 30, True)
# (name, age, is_active)
name = user_info[0] # str
age = user_info[1] # int
active = user_info[2] # bool

Lists are homogeneous—all elements have the same type:

ages: list[int] = [30, 25, 40, 35]

3. Dictionary keys and set elements

I ran into this when trying to cache location data:

# This doesn't work
cache = {
[40.7128, -74.0060]: "New York" # TypeError: unhashable type: 'list'
}

Lists can’t be dictionary keys because they’re mutable—if the list changes, the hash would change, breaking the dictionary lookup.

But tuples work:

cache = {
(40.7128, -74.0060): "New York",
(51.5074, -0.1278): "London"
}
print(cache[(40.7128, -74.0060)]) # "New York"

The same applies to set elements:

# Valid
locations = {(40.7128, -74.0060), (51.5074, -0.1278)}
# Invalid
# locations = {[40.7128, -74.0060], [51.5074, -0.1278]} # TypeError

4. Performance and memory

I compared memory usage:

import sys
my_list = [1, 2, 3, 4, 5]
my_tuple = (1, 2, 3, 4, 5)
print(f"List: {sys.getsizeof(my_list)} bytes")
print(f"Tuple: {sys.getsizeof(my_tuple)} bytes")
# Output:
# List: 120 bytes
# Tuple: 80 bytes

The tuple uses 33% less memory. This adds up when you have millions of records.

Tuple creation is also faster—tuples don’t need to allocate extra space for potential growth like lists do.

When I still use lists

Despite the benefits of tuples, I still use lists when:

  • I need to add/remove items frequently
  • The data represents a collection of similar items
  • I’m modifying data in place intentionally
# List is the right choice here
shopping_cart = ["apple", "banana", "orange"]
shopping_cart.append("milk") # Modifying makes sense

Beyond basic tuples: NamedTuple

When I need both immutability and readable field names, I use NamedTuple:

from collections import namedtuple
Color = namedtuple('Color', ['red', 'green', 'blue'])
pixel = Color(255, 128, 0)
print(pixel.red) # 255 - clearer than pixel[0]
print(pixel.green) # 128
print(pixel.blue) # 0

You get immutability with semantic field names. It’s the best of both worlds.

Visual comparison

┌─────────────────────────────────────────────────────────────┐
│ LIST │
├─────────────────────────────────────────────────────────────┤
│ Type: Homogeneous (all same type) │
│ Purpose: Collection of similar items │
│ Mutable: Yes │
│ Can be dict key: No │
│ Memory: Higher (needs space for growth) │
│ │
│ ages = [25, 30, 35, 40] │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ TUPLE │
├─────────────────────────────────────────────────────────────┤
│ Type: Heterogeneous (different types per position) │
│ Purpose: Record where position has meaning │
│ Mutable: No (immutable) │
│ Can be dict key: Yes │
│ Memory: Lower (fixed size) │
│ │
│ user = ("Alice", 30, True) │
│ # (name: str, age: int, active: bool) │
└─────────────────────────────────────────────────────────────┘

Summary

In this post, I showed why Python tuple immutability matters in practical scenarios. The key point is that tuples aren’t just “immutable lists”—they serve a different purpose.

Use tuples when:

  • Data should never change (enforcing constraints)
  • Position has semantic meaning (records, not collections)
  • You need dictionary keys or set elements
  • You want better performance and lower memory usage

Use lists when:

  • You need to modify the collection
  • It’s a collection of similar items
  • Mutation is intentional and documented

The immutability of tuples isn’t a limitation—it’s a feature that prevents bugs and makes code more predictable, especially in large codebases.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments