Lists and Tuples
Beginner⏱55 minutes📖18 min readLesson 7 of 10

Lists and Tuples

Master Python sequences: indexing, slicing, in-place list methods, sort versus sorted, aliasing and shallow copies, the shared-row trap, tuple immutability and hashability

A developer builds a three by three score grid for a puzzle prototype. She writes grid = [[0, 0, 0]] * 3, sets the centre cell with grid[1][1] = 1, and watches the whole middle column turn to 1. The board is wrong before the first move, and the code looks correct at a glance.

The cause is not the index arithmetic. The multiplication copied three references to one inner list, so every row is the same object wearing three names. A plain list copy behaves the same way in miniature, and the same misunderstanding breaks shopping carts, report tables and game state.

This lesson separates the two Python sequences beginners meet first. A list changes in place, and a tuple does not. The difference sounds small until a second name points at the same list, or a tuple turns out to contain a list that still changes.

Key terms

List
An ordered, mutable sequence written with square brackets, such as [1, 2, 3].
Tuple
An ordered, immutable sequence usually written with parentheses, such as (3, 4).
Index
A zero-based position in a sequence, where 0 is the first item and -1 is the last.
Slice
A new sequence copied from a range of positions, written start:stop:step with an exclusive stop.
Mutable
Able to change in place after creation, so methods such as append can alter the object.
Immutable
Fixed after creation, so no operation can change the object itself.
Alias
A second name bound to the same object, so a change seen through one name appears through the other.
Shallow copy
A new outer sequence whose nested objects are still shared with the original.
Hashable
Able to produce a stable hash value, which makes an object usable as a dictionary key or set member.
Unpacking
Assigning the items of a sequence to separate names in one statement.
Comprehension
A compact expression that builds a new list from an iterable in a single line.

This lesson is the seventh stop of the course. By the end you will index and slice both sequence types, predict which list calls return None, and trace two names that point at one object. You will also see why a tuple can key a dictionary while a list cannot, and every claim here was checked against the Python 3.15 documentation.

1. Two Sequences, One Shape

A list is an ordered, mutable sequence written with square brackets, and a tuple is an ordered, immutable sequence usually written with parentheses. Both keep their items in a fixed order, both allow duplicates, and both answer to the same indexing and slicing rules.

python
fruits = ["apple", "banana", "cherry"] point = (3, 4) mixed = [1, "hello", 3.14, True] nested = [[1, 2], [3, 4]] single = (1,)

The square brackets create a list, and the parentheses create a tuple. The trailing comma in (1,) is not decoration; without it Python reads the expression as the integer 1 inside grouping parentheses.

Both constructors accept any iterable, which makes conversion between the types direct. A string hands out characters, a range hands out numbers, and the constructor collects whatever arrives.

python
print(list("abc")) # ["a", "b", "c"] print(tuple([1, 2, 3])) # (1, 2, 3) print(list(range(3))) # [0, 1, 2]

The shared shape is the useful part. Everything you learn about indexing a list also works on a tuple, and the difference appears only when you try to change the object.

Question
List
Tuple
Can it change after creation?
yes
no
Brackets
square
round
One item
[1]
(1,)
Typical use
cart, queue, log
point, colour, row
Dictionary key
never
only when every item is hashable
ℹ️Info

The two types are not rivals. A list of tuples is a normal table shape, because each row is a fixed record while the table itself grows and shrinks.

2. Indexing: Positions Count From Zero

An index is a whole number that names one position. Python counts from 0, so the first item sits at index 0 and the last item sits one position before the length.

python
fruits = ["apple", "banana", "cherry", "date"] print(fruits[0]) # apple print(fruits[2]) # cherry print(fruits[-1]) # date print(fruits[-2]) # cherry

A negative index counts from the end, so -1 always names the last item. Python resolves that negative position before it reads the list, which makes both forms equivalent for a reader.

The call len(fruits) returns 4, so valid positions run from 0 to 3. An index of 4 or -5 raises IndexError, because the sequence has no such position.

python
>>> fruits[4] Traceback (most recent call last): File "<stdin>", line 1, in <module> IndexError: list index out of range

An out-of-range index raises an error rather than returning None, which is a deliberate choice. A missing item should be loud, because a quiet None would travel through later code and fail far from the cause.

ℹ️Note

📚 Did you know? Negative indexes are not a separate storage area. fruits[-1] is translated to fruits[len(fruits) - 1], so it follows the same bounds rule as a positive index.

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A list named items holds ["a", "b", "c", "d"]. A program prints items[-2] and then items[1]. Which two values are printed, in that order?

3. Slicing: A Window With an Exclusive Stop

A slice copies a window of a sequence, and the stop position is exclusive. The expression items[1:3] returns positions 1 and 2, then stops before position 3.

python
letters = ["a", "b", "c", "d", "e"] print(letters[1:4]) # ["b", "c", "d"] print(letters[:3]) # ["a", "b", "c"] print(letters[3:]) # ["d", "e"] print(letters[:]) # a full copy print(letters[::2]) # ["a", "c", "e"] print(letters[::-1]) # ["e", "d", "c", "b", "a"]

Omitted bounds default to the ends, and a step of 2 skips every second item. A negative step walks backward, which is the usual way to reverse a sequence without changing the original.

The slice returns a new list for a list and a new tuple for a tuple. It copies the references to the items, not the items themselves, a detail that matters again in section 6.

The diagram below follows one slice from a four-item list. Only positions 1 and 2 cross into the result, because the stop at 3 is excluded.

100%
Description

A flowchart for the section 3. Slicing: A Window With an Exclusive Stop, showing the path and decision points described in the surrounding text.

The original list keeps all four items, and the new list can be changed without touching it. A slice assignment goes further and can replace or delete a range in the original list.

python
scores = [70, 80, 90, 95] scores[1:3] = [100] print(scores) # [70, 100, 95]

The two scores at positions 1 and 2 became one score, so the list also became shorter. Slice assignment is a list-only feature, because a tuple has no positions that can be reassigned.

⚠️Warning

items[1:3] holds two items, not three. The stop is the first position that is not copied, so read the slice aloud as from one up to but not including three.

Practice Question
How to answer

Multiple-choice question with 2 options. Select one option, then choose Check Answer.

A slice of a list returns a new list, and the stop index is included in that new list.

4. List Methods and Their Return Values

A method is a function attached to an object, and most list methods change the list in place. The return value is the part beginners ignore, and it is the part that causes silent bugs.

Method
What it does
What it returns
append(item)
adds one item to the end
None
extend(iterable)
adds every item from another iterable
None
insert(i, item)
places one item before index i
None
remove(value)
deletes the first equal value
None
pop()
removes and returns the last item
the removed item
pop(i)
removes and returns the item at i
the removed item
clear()
removes every item
None
index(value)
finds the first position
the position
count(value)
counts equal items
the count
sort()
reorders the list in place
None
reverse()
reverses the list in place
None
python
cart = [] cart.append("laptop") cart.append("mouse") print(len(cart)) # 2 last = cart.pop() print(last) # mouse print(cart) # ["laptop"]

The call append adds one item, and extend adds every item from another collection. Passing a string to extend adds its characters, while append adds the whole string as one item.

python
letters = ["a", "b"] letters.extend("cd") print(letters) # ["a", "b", "c", "d"] letters.append("cd") print(letters) # ["a", "b", "c", "d", "cd"]

The call remove raises ValueError when the value is absent, and pop raises IndexError when the list is empty. Both failures are useful because they stop a program before it works with a wrong value.

The call insert places an item before the given index, not after it. An index equal to the length appends, and a negative index counts from the end in the usual way.

Matching Question
How to answer

Matching exercise with 6 pairs. Match each item on the left with its answer on the right.

Match each list operation to what it returns or changes.

append(item)
Drop here
extend(iterable)
Drop here
pop()
Drop here
remove(value)
Drop here
sort()
Drop here
sorted(iterable)
Drop here

Drag answers from here:

returns None and adds one item
returns None and adds every item
returns the removed last item
returns None and deletes the first equal value
returns None and reorders in place
returns a new sorted list
💡Tip

Use append for one item and extend for a collection. If you remember only one rule, remember that a method which changes a list usually returns None.

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A program runs x = [1, 2], then result = x.append(3), then prints result and x. What does it print?

5. sort() and sorted(): One Changes, One Returns

The two sorting tools differ in who owns the result. The method sort() reorders the list in place and returns None, while the built-in sorted() returns a new sorted list and leaves the input alone.

python
grades = [85, 92, 78, 90, 88] print(sorted(grades)) # [78, 85, 88, 90, 92] print(grades) # original order is unchanged result = grades.sort() print(grades) # [78, 85, 88, 90, 92] print(result) # None

The first call leaves grades untouched, and the second call changes it. The name result holds None because sort() reports success by changing the object, not by returning a new one.

Both calls accept reverse=True, and both accept a key function that extracts the value to compare. Sorting is stable, so items with equal keys keep their original relative order.

Call
Changes the input?
Returns
grades.sort()
yes
None
sorted(grades)
no
a new list
grades.reverse()
yes
None
reversed(grades)
no
a reverse iterator

The call reverse() is the in-place partner of reversed(), and the same return value rule applies. The functions len, min, max and sum read a sequence without changing it.

python
grades = [85, 92, 78, 90, 88] print(sum(grades)) # 433 print(min(grades)) # 78 print(max(grades)) # 92 print(sum(grades) / len(grades)) # 86.6

Five grades add to 433, so the mean is 86.6. The numbers stay exact until the division at the end, which is a small habit worth keeping when totals matter.

The state diagram shows the two paths through a list. The left path stays on the original object, and the branch to the right builds a new list that the original never sees.

100%
Description

A state diagram for the section 5. sort() and sorted(): One Changes, One Returns, showing the states and transitions described in the surrounding text.

📌Important

A line such as grades = grades.sort() is a common mistake. It replaces the list with None, and the next line that indexes grades raises TypeError instead of showing a sorted list.

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A program runs nums = [3, 1, 2], then result = nums.sort(), then prints nums and result. What does it print?

6. Aliasing: Two Names, One Object

Assignment binds a name to an object, and it never copies the object. Two names can point at one list, which is called aliasing, and a change through either name is visible through the other.

python
a = [1, 2] b = a b.append(3) print(a) # [1, 2, 3] print(b) # [1, 2, 3]

There is one list and two names. The append changes the only list that exists, so both prints show the same three items.

Copying needs an explicit instruction. A full slice, the copy() method and list(a) each build a new outer list, and all three produce a shallow copy.

python
a = [1, 2] b = a[:] c = a.copy() d = list(a) b.append(3) print(a, b, c, d) # [1, 2] [1, 2, 3] [1, 2] [1, 2]

A shallow copy shares any nested objects with the original. If the list contains another list, the copy points at the same inner list, so a change inside it appears in both places.

The sequence diagram follows two names and one list object. The copy branch is the only path that gives the second name its own outer list.

100%
Description

A sequence diagram for the section 6. Aliasing: Two Names, One Object, showing the order of messages and replies described in the surrounding text.

⚠️Warning

The statement b = a is not a copy. Use b = a.copy() when the second name needs its own outer list, and use a deep copy from the copy module only when nested objects must be independent too.

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A program runs a = [1, 2], then b = a, then b.append(3), then c = a[:], then c.append(4). What do a and c hold at the end?

7. The Shared Row Trap

Repetition with * copies references, and that rule turns a nested list into a trap. The expression grid = [[0]] * 3 builds three references to one inner list, so every row is the same object.

python
grid = [[0]] * 3 grid[0][0] = 9 print(grid) # [[9], [9], [9]]

The outer list has three positions, and all three hold a reference to one inner list. Changing position 0 of that inner list changes the value that all three rows read.

The fix is a comprehension, which runs the inner expression once per row and builds a fresh list each time. The underscore is a convention for a loop name that the expression does not use.

python
grid = [[0] for _ in range(3)] grid[0][0] = 9 print(grid) # [[9], [0], [0]]

Three separate inner lists now exist, so the change stays in row 0. The diagram shows the difference between one shared object and three independent objects.

100%
Description

A flowchart for the section 7. The Shared Row Trap, showing the path and decision points described in the surrounding text.

Flat repetition is safe when the repeated item is immutable. The expression [0] * 3 makes three positions that hold the integer 0, and integers cannot change, so the result behaves as most people expect.

🚨Danger

The trap is silent. There is no error and no warning, because the code is valid; only the output is wrong. Print the object id of each row while debugging to see whether they are the same list.

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A program runs grid = [[0]] * 3, then grid[0][0] = 9, then prints grid. Which value is printed?

8. Tuples: Fixed Shape, Flexible Contents

A tuple is immutable, so it has no append, remove or sort, and item assignment raises TypeError. It still supports indexing, slicing, concatenation, repetition, membership, len, count and index.

python
point = (3, 4) print(point[0]) # 3 print(point[-1]) # 4 print(len(point)) # 2 print(point.count(4)) # 1 print(point.index(3)) # 0 # point[0] = 5 raises TypeError

The tuple stores the same kind of ordered items as a list, and the read operations are the same. The missing operations are the point of the type, because they let a reader trust that the shape will not change.

The one-item syntax is the first trap. Parentheses group an expression, and the comma is what makes a tuple, so a one-item tuple needs the trailing comma.

python
single = (1,) not_a_tuple = (1) print(type(single)) # <class 'tuple'> print(type(not_a_tuple)) # <class 'int'>

An empty tuple is simply (), and a tuple with two or more items needs no trailing comma. The comma rule only looks strange because parentheses already have another job in arithmetic.

Immutability is shallow. A tuple can contain a list, and that list can still change, so the tuple is not a deep freeze.

python
record = ("Ada", [92, 88]) record[1].append(95) print(record) # ("Ada", [92, 88, 95])

The tuple still points at the same list, and the list changed in place. The tuple's own positions never changed, which is all that immutability promises.

ℹ️Note

📚 Did you know? sys.version_info is a named tuple with fields such as major, minor and micro. It reads like an object and still unpacks like a plain tuple.

Practice Question
How to answer

Multiple-choice question with 2 options. Select one option, then choose Check Answer.

A tuple cannot be changed, so a list stored inside that tuple also becomes read-only.

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A program runs t = (5), then prints type(t). What does it print, and why?

9. Packing, Unpacking and the Starred Rest

Packing writes several values into one tuple, and unpacking assigns the items of a sequence to several names. The two directions use the same comma syntax, which is why they are easy to read together.

python
point = 3, 4 # packing x, y = point # unpacking print(x, y) # 3 4

The right side builds a tuple, and the left side takes it apart. Python evaluates the right side first, so the names on the left can be new or existing.

The counts must match unless a starred name absorbs the rest. A single star collects every remaining item into a list, and that list may be empty.

python
first, *rest = [1, 2, 3, 4] print(first) # 1 print(rest) # [2, 3, 4] head, *middle, tail = "abcde" print(head, middle, tail) # a ["b", "c", "d"] e

The starred name is always a list, even when it collects one item or none. PEP 448 generalized the same star syntax to function calls and displays, so the idea travels beyond assignment.

python
rest = [2, 3, 4] print([0, *rest]) # [0, 2, 3, 4]

A star inside a list display unpacks the items of another iterable into the new list. That is the same idea in a different position, and it saves a loop when the values are already collected.

Swapping needs no temporary variable, because the right side builds a tuple before either name is rebound. The technique works for any two names and is one of the small pleasures of Python.

python
a = 1 b = 2 a, b = b, a print(a, b) # 2 1

The RGB example shows why a tuple suits a fixed record. The three channels keep a documented order, and the unpacking line reads like the definition of the value.

python
red = (255, 0, 0) r, g, b = red dim = (r * 50 // 100, g * 50 // 100, b * 50 // 100) print(dim) # (127, 0, 0)

Fifty percent of 255 is 127 after integer division, and the other channels stay at zero. A list would hold the same numbers, but a tuple signals that the three positions belong together.

Fill in the Blanks
How to answer

Fill-in-the-blank exercise with 3 blanks. Type the missing words, then check your answer.

Complete the slice and the starred unpacking for a four-item list named items.

The slice items[blank:blank] copies the two middle items. The statement first, blankrest = items puts the last three items in a list.

Word Bank, drag words into the blanks above:

1
3
*
0
2
**
💡Tip

Use unpacking when a tuple has a known shape, and use indexing when the position depends on data. A line such as latitude, longitude = point documents the order better than point[0] and point[1].

10. Hashability: Keys Need a Stable Value

A hashable object produces a stable hash value, which lets a dictionary or set find it quickly. Tuples are hashable when every item inside them is hashable, while lists are never hashable.

python
print(hash((1, 2))) # a stable integer # hash([1, 2]) raises TypeError # hash((1, [2])) raises TypeError

The first tuple qualifies because both items are integers. The list fails because it can change, and a changing key would break the lookup structure that depends on it.

A tuple is therefore a natural dictionary key when its positions have meaning. Coordinates are the classic example, and the key holds two values that belong together.

python
locations = {} locations[(48.8584, 2.2945)] = "Eiffel Tower" print(locations[(48.8584, 2.2945)]) # Eiffel Tower

The two floats form one key, and the dictionary finds the value by hashing the tuple. A list key would raise TypeError: unhashable type: 'list', because the dictionary cannot trust a value that may change.

A set of tuples also removes duplicate records, because equal tuples hash to the same value. A set of lists cannot exist for the same reason, so the tuple is the tool for a deduplicated collection of records.

⚠️Warning

A tuple is hashable only when all of its items are hashable. The tuple (1, 2) works, while (1, [2]) fails, so check the contents before you use a tuple as a key.

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A program needs a dictionary key that represents one point with an x value and a y value. Which value can be used as that key?

11. Choosing a List or a Tuple

The choice follows meaning first and performance second. Use a list when the collection changes, and use a tuple when the shape is fixed and each position has a documented meaning.

Situation
Better choice
Reason
Shopping cart
list
items arrive and leave
Queue of jobs
list, or deque for large queues
order matters and the collection changes
Point or coordinate
tuple
fixed pair with a known order
RGB colour
tuple
three channels in a fixed order
Database row
tuple
column order carries meaning
Dictionary key
tuple
must be hashable
Grid of rows
list of lists
rows can be replaced or extended

A list of tuples is a common table shape, because each row is a fixed record while the table itself grows and shrinks. That combination uses both types for the job each one does best.

PEP 8 does not force a tuple over a list, and the style guide has no rule about the choice. Pick the type that tells the next reader what the code intends to do with the data.

The flowchart turns the decision into three questions. A yes to change leads to a list, and a fixed shape with meaningful positions leads to a tuple.

100%
Description

A flowchart for the section 11. Choosing a List or a Tuple, showing the path and decision points described in the surrounding text.

A tuple is generally a little smaller than a list of the same length, but the difference is small and version dependent. Choose by meaning and mutation needs, not by a memory micro-benchmark.

ℹ️Info

A tuple does not guarantee that the value never changes. It guarantees that the tuple's own positions never change, while any mutable object inside can still be modified.

12. A First Look at Comprehensions and namedtuple

Two tools make the sequence types more expressive, and both deserve a full lesson later. A list comprehension builds a new list from an iterable in one expression, and a named tuple gives a tuple readable field names.

python
squares = [n * n for n in range(5)] print(squares) # [0, 1, 4, 9, 16] grid = [[0] for _ in range(3)] print(len(grid), len(grid[0])) # 3 3

The first line reads as one square for each n in the range, and the second builds independent rows. A comprehension is not a faster loop here; it is a compact expression with a clear result.

The namedtuple factory builds a tuple subclass with named fields, and the result stays immutable and unpackable. The collections module is part of the standard library, so no install is needed.

python
from collections import namedtuple Point = namedtuple("Point", ["x", "y"]) p = Point(3, 4) print(p.x, p.y) # 3 4 print(p[0]) # 3 x, y = p print(x, y) # 3 4

The field names make the code readable, while indexing and unpacking still work because the object is a tuple. A later lesson returns to comprehensions with the detail they deserve.

💡Tip

Treat this section as a map, not a full lesson. You will meet comprehensions again with filtering and conditions, and named tuples again when a plain tuple needs better field names.

13. Lists and Tuples in the Python 3.15 World

2026 Updates (as of November 2026)

ℹ️Note

What changed recently, and why older tutorials may look different. Every line below was checked against a primary source in November 2026:

  • Python 3.15.2 is the current stable release (3.15.0 shipped on 2026-10-09, and 3.15.2 followed on 2026-10-25). List and tuple behaviour is unchanged in 3.15, so every example here runs as written.
  • Python 3.10 reached end of life on 2026-10-01. The sequence types in this lesson are older than every supported release, so the syntax is not tied to one version.

The sequence types are old and stable, which is good news for code written years ago. A list or tuple you write today will still run on the current release, and the return value rules will not move.

ℹ️Note

📚 Did you know? A tuple is not automatically smaller for every size on every platform. The exact byte counts come from the implementation, so treat any quoted number as a snapshot rather than a rule.

Real-World Case Studies

Netflix. The Netflix Tech Blog in April 2019 described Python across the full content lifecycle, including the tooling behind the Open Connect CDN. Catalog tools and encoding checks walk titles as sequences, so the list or tuple choice sits inside that daily work.

Database drivers. PEP 249 defines the Python Database API, and its fetch methods return a row as a sequence. A row is a fixed record whose column order carries meaning, which is exactly the tuple case in the table above.

GeoJSON. RFC 7946 defines a position as an array of numbers with longitude first and latitude second. Swapping those two values is a real mapping bug, so a tuple with a documented order is a useful guard against a careless edit.

Practice Questions

Answer these before the next lesson; the final two questions reach back to earlier lessons.

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A program runs items = [5, 10, 15, 20], then result = items[1:3] if len(items) == 4 else items[:2], then prints result. Which list is printed?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A program runs pairs = list(enumerate(["red", "green"], start=1)). Which value does pairs hold?

What Comes Next

You can now read and change ordered collections with confidence. The next lesson moves to dictionaries and sets, where values are found by key instead of position, and the tuples you just learned become useful keys.

Before moving on, the video below is a full beginner course that replays this material in a second voice. Its list and tuple chapters are a good parallel reference when you want the same ideas from another teacher.

A long-form beginner course with dedicated sections on lists, tuples and sequence operationsWatch on YouTube
Description

A video walkthrough that supports the section What Comes Next and repeats its main ideas in a second voice.

✅Success

Key Takeaways:

  1. A list is an ordered, mutable sequence, and a tuple is an ordered, immutable sequence. Both share indexing, slicing and unpacking.
  2. Indexing starts at 0, negative indexes count from the end, and an out-of-range index raises IndexError.
  3. A slice copies a new sequence with an exclusive stop, so items[1:3] returns two items and never three.
  4. append adds one item, extend adds many, and insert places one item before an index.
  5. sort() and reverse() change a list in place and return None, while sorted() returns a new list and leaves the input alone.
  6. Assignment aliases an object, and a slice or copy() makes only a shallow copy that still shares nested objects.
  7. [[0]] * 3 shares one inner list across three rows, so use a comprehension to build independent rows.
  8. A one-item tuple needs a trailing comma, and tuple immutability is shallow because a contained list can still change.
  9. Packing and unpacking move values between tuples and names, *rest collects the leftovers, and a, b = b, a swaps two names.
  10. A tuple is hashable when every item is hashable, so it can key a dictionary, while a list never can.

References

Every factual claim links to its source, with the access date printed so the source can be rechecked.

  1. [1]Data StructuresDocumentation

    Python Software Foundation · Accessed 2026-11-05

  2. [2]Built-in typesDocumentation

    Python Software Foundation · Accessed 2026-11-05

  3. [3]Built-in functionsDocumentation

    Python Software Foundation · Accessed 2026-11-05

  4. Python Software Foundation · Accessed 2026-11-05

  5. Python Software Foundation · Accessed 2026-11-05

  6. Python Software Foundation · Accessed 2026-11-05

  7. Netflix Tech Blog · Accessed 2026-11-05

  8. Python Software Foundation · Accessed 2026-11-05

  9. IETF · Accessed 2026-11-05

Progress70%