Problem Solved

Python · list vs tuple

Home › Courses › Python › Problems › List vs tuple
🐍 Concept · 3 min read

List vs tuple.
Mutable or not.

The most common Python comparison. Lists and tuples look similar — both are ordered collections — but they behave differently. Lists are mutable, tuples are immutable. That one difference drives everything else: memory, speed, hashability, and the semantics of "this data should not change." This page covers the difference, when each wins, and the single-element tuple gotcha.

Short answer: Lists are mutable [1, 2, 3] — add, remove, change. Tuples are immutable (1, 2, 3) — created once, can't be changed. Tuples are ~27% smaller, faster to create, and hashable (can be dict keys). Lists can't be dict keys. Use tuples for fixed records — coordinates, RGB, dict keys, function returns. Use lists for collections that grow or change. Single-element tuple needs a trailing comma: (1,), not (1).

✅ The core difference

Mutability is everything.

mutability.py
# List — mutable
nums = [1, 2, 3]
nums.append(4)          # ✅ works
nums[0] = 99            # ✅ works
del nums[1]             # ✅ works
print(nums)             # [99, 3, 4]

# Tuple — immutable
coords = (10, 20)
coords[0] = 99          # ❌ TypeError: 'tuple' object does not support item assignment
coords.append(30)       # ❌ AttributeError: 'tuple' object has no attribute 'append'
del coords[0]           # ❌ TypeError

# Tuple can be a dict key
point = (10, 20)
locations = {point: 'Lahore'}
print(locations[(10, 20)])  # 'Lahore'

# List can't
try:
    locations[[10, 20]] = 'Karachi'
except TypeError as e:
    print(e)            # unhashable type: 'list'

# Tuple can be a set member
seen = {(1, 2), (3, 4)}
print((1, 2) in seen)   # True

# List can't
try:
    seen = {[1, 2]}
except TypeError as e:
    print(e)            # unhashable type: 'list'
What mutability enables:

Lists:
  → append, extend, insert
  → remove, pop, clear
  → item assignment (lst[0] = x)
  → del lst[i]
  → sort in place
  → reverse in place
  → slice assignment (lst[1:3] = [...])

Tuples:
  → no mutation methods
  → no item assignment
  → no del on items
  → no sort in place (use sorted(t) — returns list)
  → no reverse in place (use reversed(t) — returns iterator)
  → no slice assignment

What immutability enables:

Tuples:
  → hashable (dict keys, set members)
  → thread-safe (no locks needed)
  → signal of intent ("this is fixed")
  → safe as default arguments
  → slight memory/speed advantage
  → can be used as cached keys

Lists:
  → not hashable
  → not safe as default arguments
  → need locks if shared across threads
  → mutable state is fine, but has a cost

The rule:
  Use a list when you need to change the collection.
  Use a tuple when the collection is fixed.
  The type communicates intent.

Why tuples can be dict keys:

  Python's dict uses a hash table.
  To look up a key, it hashes the key.
  Hash requires the value to be immutable —
  otherwise the hash could change after insertion,
  and lookups would break.

  Tuples are immutable → hash is stable → safe as keys.
  Lists are mutable → hash would change → not allowed.

Same logic for set members.
Same logic for frozenset vs set.

Hashability is the foundation of Python's
data structures. Immutability is what makes
hashability possible.

✅ Memory and speed

Tuples are smaller and faster. Small margins.

memory.py
import sys

# Empty container
lst = []
tup = ()

print(sys.getsizeof(lst))  # 56  (empty list)
print(sys.getsizeof(tup))  # 40  (empty tuple)

# Five elements
lst = [1, 2, 3, 4, 5]
tup = (1, 2, 3, 4, 5)

print(sys.getsizeof(lst))  # 120  (with over-allocation)
print(sys.getsizeof(tup))  # 80

# The difference: 40 bytes on 5 elements.
# About 33% smaller for tuples.

# One hundred elements
lst = list(range(100))
tup = tuple(range(100))

print(sys.getsizeof(lst))  # 856  (over-allocated)
print(sys.getsizeof(tup))  # 840

# The difference shrinks as n grows.
# For 100 elements, ~2% smaller.

# Speed: tuple creation is faster
import timeit

print(timeit.timeit('[1, 2, 3]', number=1000000))  # ~0.05s
print(timeit.timeit('(1, 2, 3)', number=1000000))  # ~0.03s

# Tuple is ~40% faster to create.
# But the absolute difference is 20ms per million
# operations — irrelevant for typical code.

# Iteration speed is nearly identical.
# Indexing is nearly identical.
# The difference only matters in tight loops
# with millions of operations, and even then
# it's small.
Why lists are bigger:

Lists over-allocate to support append.
When a list grows, Python allocates more space
than needed — usually 1.125x to 1.25x — so
the next append doesn't require a resize.

  [1, 2, 3]  → allocated for 3, room for 3-4
  Append 4   → room already allocated
  Append 5   → resize, allocate ~10 slots

The over-allocation is why lists are ~33% bigger
for small collections.

Tuples are fixed size.
When you create (1, 2, 3), Python allocates
exactly 3 slots. No over-allocation.

The memory difference matters when:
  → You have millions of small containers
  → You're reading a large dataset into memory
  → You're caching many keys

It doesn't matter when:
  → You have a few thousand containers
  → You're processing data incrementally
  → The containers are small relative to
    the objects they hold

For 99% of code, the difference is noise.
Choose based on semantics, not micro-optimization.

The real performance win:

Tuples as dict keys:

  # Using a list as a key — impossible
  cache[[1, 2]] = 'value'  # TypeError

  # Using a tuple as a key — works
  cache[(1, 2)] = 'value'  # OK

  # This is the memoization pattern:
  @cache
  def fn(a, b):  # *args becomes a tuple
      ...

The performance benefit isn't from tuple vs list
memory — it's from being able to use the tuple
as a key at all. That's the real reason.

✅ When to use which

The decision rules.

when-to-use.py
# ✅ Tuple — fixed record
point = (10, 20)
rgb = (255, 128, 0)
date = (2026, 11, 7)

# ✅ Tuple — dict key
cache = {}
cache[(10, 20)] = 'Lahore'
cache[(30, 40)] = 'Karachi'

# ✅ Tuple — function returning multiple values
def min_max(nums):
    return min(nums), max(nums)

low, high = min_max([3, 1, 4, 1, 5, 9, 2, 6])

# ✅ Tuple — *args
def fn(*args):  # args is a tuple
    return sum(args)

# ✅ Tuple — as default argument (safe)
def process(data, options=()):
    for opt in options:
        ...

# ❌ List as default — mutation trap
def bad(data, items=[]):  # BUG: shared across calls
    items.append(data)
    return items

# ✅ List — collection that changes
queue = []
queue.append('task1')
queue.append('task2')
queue.pop(0)

# ✅ List — needs sort in place
scores = [3, 1, 4, 1, 5]
scores.sort()  # returns None, modifies in place

# ✅ List — needs item assignment
scores[0] = 100

# ✅ List — needs append/extend/insert
results = []
for x in range(10):
    results.append(x * x)

# The decision:
# Does the collection change? → list
# Is it a fixed record? → tuple
# Does it need to be a key? → tuple
# Is it returned from a function? → tuple
# Does it need to be sorted/mutated? → list
# Is it *args or **kwargs? → tuple
The three most important cases:

1. Function return values — tuple
   def get_user():
       return 'Alice', 30, 'alice@example.com'
   
   name, age, email = get_user()
   
   Why tuple? Because the return is a fixed
   record. You're not going to append to it.
   Python creates a tuple by default when you
   return multiple values with a comma.
   
   Also: the caller can ignore fields with _:
   name, _, email = get_user()

2. Dict keys — tuple
   cache = {(10, 20): 'Lahore'}
   
   Why? Because lists aren't hashable.
   Tuples are. If you need a composite key,
   you need a tuple.
   
   This applies to:
     - Coordinates
     - Multi-part IDs
     - Cached function arguments
     - Grouping by multiple fields

3. Default arguments — tuple (never list)
   def good(items=()):    # safe
       ...
   
   def bad(items=[]):     # BUG
       items.append(1)    # mutates the default
       return items
   
   The list default is evaluated once and
   shared across all calls. The tuple default
   is safe because it can't be mutated.
   
   The standard fix is None + check:
   def good(items=None):
       if items is None:
           items = []
   
   But tuple-as-default is cleaner when the
   function never mutates the argument.

The tuple is the "safer default."
It's immutable, hashable, and signals intent.
Use it whenever you don't need to change
the collection.

The list is for "collection that changes."
Use it when you're building, accumulating,
sorting, or filtering in place.

Both are correct. The type tells the reader
which one you mean.

Don't use a list as a default argument. This is the most famous Python gotcha, and it's still everywhere in code reviews. def bad(items=[]): evaluates the [] once — at function definition time — and shares it across all calls. Call the function twice, appending to the list, and you get the previous call's items. The fix: use None as the default and create a new list inside: def good(items=None): if items is None: items = []. Or use a tuple, which is immutable and can't be mutated. Either works. Just don't use a mutable default argument.

⚠️ Common mistakes

The list/tuple mistakes that cost real time.

1. (1) instead of (1,)

Single-element tuple needs a trailing comma. Fix: (1,).

2. List as default argument

Mutable default is shared across calls. Fix: None + check, or tuple.

3. Trying to hash a list

{[1,2]: 'x'} — TypeError. Fix: {(1,2): 'x'}.

4. Trying to append to a tuple

No append method. Fix: create a new tuple, or use a list.

5. Expecting sort() to work on a tuple

Tuples are immutable. Fix: sorted(t) returns a list.

6. Assuming tuple of mutables is immutable

Contents can still change. Fix: only immutable items for true immutability.

7. Converting to tuple without a reason

Loses mutability you might need. Fix: convert only at the boundary where immutability matters.

8. Using a tuple for a growing collection

Every append creates a new tuple. Fix: use a list, convert to tuple when done.

❓ Frequently asked

What is the difference between a list and a tuple in Python?

Three differences. (1) Lists are mutable — you can add, remove, and change items. Tuples are immutable — once created, they can't be changed. (2) Lists use square brackets [1, 2, 3]. Tuples use parentheses (1, 2, 3). (3) Tuples are slightly smaller and faster to create. Tuples can be dictionary keys and set elements; lists can't.

Why would I use a tuple instead of a list?

Four reasons. (1) Immutability signals intent — a tuple says 'this shouldn't change.' (2) Tuples are hashable — they can be dict keys and set members. (3) Tuples are faster and smaller. (4) Tuples work where an API requires an immutable type. Common cases: coordinates, RGB colors, database rows, function return values.

Are tuples really faster than lists?

Yes, but the difference is small. In CPython, tuples are allocated with a fixed size — no over-allocation, no resize support. Lists over-allocate to support append and pop. But the difference is in nanoseconds — for most code, it's not the reason to choose one. The reason to choose is semantics: is this data supposed to change?

Can a tuple contain mutable objects?

Yes. A tuple is immutable in that its slots can't be reassigned — but the objects inside can be mutable. Example: t = ([1, 2], [3, 4]); t[0].append(5) works. This is the 'tuple of mutables' trap. If you need true immutability, use only immutable types inside the tuple. A tuple is hashable only if all its contents are hashable.

How do I create a tuple with one element?

Add a trailing comma: (1,) — not (1). Without the comma, (1) is just the integer 1 in parentheses. The comma is what makes it a tuple, not the parentheses. type((1)) is int, type((1,)) is tuple. type(()) is tuple (empty tuple).

When should I convert a list to a tuple?

When you need a hashable version (dict key, set member), when you're done modifying a collection and want to signal it's now fixed, or when you're passing data to an API that requires a tuple. The conversion itself is O(n) — it copies the elements. Don't convert repeatedly; convert once at the boundary where immutability is needed.

🔗 Continue learning

🗺️ Python learning path
The full roadmap from install to first project — every concept in order

Next in this series:

🏗️ if __name__ == "__main__"
Why every script has this line, and what happens when you remove it
← All Python problems
Problem solved · Python