Lists and tuples are the two general-purpose sequence types in Python. Lists are mutable, so you can grow, shrink, and modify them in place. Tuples are immutable, so they are hashable, slightly faster, and signal that the contents should not change. This lesson covers every operation you will use in real code.
Creating lists and tuples
Use square brackets for a list, parentheses for a tuple. A trailing comma is required for a one-element tuple. An empty tuple is just (). A list can be empty, contain mixed types, or be nested.
numbers = [1, 2, 3, 4, 5]
mixed = ["Asha", 28, True, 3.14]
empty = []
nested = [[1, 2], [3, 4]]
point = (3, 4) # 2D point
one_item = ("solo",) # one-element tuple, note the trailing comma
empty_tuple = ()
rgb = (255, 128, 0)
Indexing and slicing
Indexing is zero-based. Negative indices count from the end. Slicing returns a new sequence with a start, stop, and optional step. The stop is exclusive. Leaving start or stop out defaults to the beginning or end.
items = ["a", "b", "c", "d", "e"]
print(items[0]) # a
print(items[-1]) # e
print(items[1:4]) # ['b', 'c', 'd']
print(items[:3]) # ['a', 'b', 'c']
print(items[::2]) # ['a', 'c', 'e']
print(items[::-1]) # ['e', 'd', 'c', 'b', 'a']
# Same for tuples
point = (10, 20, 30)
print(point[:2]) # (10, 20)
Mutating a list in place
Lists support in-place modification. Append adds to the end, extend concatenates another iterable, insert places at an index, and pop removes by index and returns the value.
tasks = ["write", "review"]
tasks.append("deploy")
print(tasks) # ['write', 'review', 'deploy']
tasks.extend(["test", "monitor"])
print(tasks) # ['write', 'review', 'deploy', 'test', 'monitor']
tasks.insert(1, "plan")
print(tasks) # ['write', 'plan', 'review', 'deploy', 'test', 'monitor']
last = tasks.pop()
print(last, tasks) # monitor ['write', 'plan', 'review', 'deploy', 'test']
Common list mistakes
Two list surprises bite newcomers. First, the aliasing trap. Assigning a list to another variable does not copy it. Second, the multiplication trap. Multiplying a list of mutable objects creates references, not copies.
# Aliasing, not a copy
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4] -- a was modified too!
# Fix: copy explicitly
b = a.copy() # or a[:] or list(a)
b.append(5)
print(a) # [1, 2, 3, 4]
print(b) # [1, 2, 3, 4, 5]
# The multiplication trap
rows = [[0] * 3] * 3
rows[0][0] = 99
print(rows) # [[99, 0, 0], [99, 0, 0], [99, 0, 0]] -- all three rows are the same inner list!
# Fix: use a comprehension
rows = [[0] * 3 for _ in range(3)]
rows[0][0] = 99
print(rows) # [[99, 0, 0], [0, 0, 0], [0, 0, 0]]
When to choose a tuple over a list
Use a tuple when the collection should not change: a point, a date, a record returned from a function. Tuples can be dict keys because they are hashable. Tuples are also slightly smaller in memory and a bit faster to iterate.
# Tuple as a dict key (a list cannot do this)
grid_costs = {
(0, 0): 1.0,
(1, 0): 1.5,
(0, 1): 1.5,
(1, 1): 2.0,
}
print(grid_costs[(1, 1)]) # 2.0
# Tuple unpacking for clean code
name, age, role = ("Asha", 28, "admin")
print(name, age, role)
A tour of useful list and tuple methods
Lists ship with reverse, sort, count, index, copy, clear, and remove. Tuples ship with only count and index because they are immutable. Use sorted for a new sorted list, or list.sort for an in-place sort.
nums = [3, 1, 4, 1, 5, 9, 2, 6]
print(nums.count(1)) # 2
print(nums.index(9)) # 5
print(sorted(nums)) # [1, 1, 2, 3, 4, 5, 6, 9]
nums.sort(reverse=True)
print(nums) # [9, 6, 5, 4, 3, 2, 1, 1]
# Named tuple for self-documenting records
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y) # 3 4
Common pitfalls with shared references
Once you start storing mutable objects inside lists, copying becomes more nuanced. Calling list.copy() or list[:] or list(...) gives you a shallow copy: the outer list is fresh, but its elements are still the same objects. That is fine for a list of integers, but for a list of dicts or lists, mutating an inner dict in the copy also mutates the original. Reach for copy.deepcopy in that case, or restructure your data so the inner objects are immutable (frozen dicts, tuples, dataclasses with frozen=True). A second trap is passing a list as a default argument to a function: def add(item, bucket=[]): appends to the same bucket forever. Default to None and create the list inside the body instead.
Comparison with JavaScript arrays and C++ std::vector
Python lists are closer to JavaScript Array than to typed arrays. JavaScript arrays are always objects with prototype methods, while Python lists are sequence types backed by a contiguous array of pointers. That means Python lists can hold heterogeneous types, but the contiguous layout is what makes list.sort() so fast: it sorts in place with no extra allocations. C++ std::vector is the nearest analogue: contiguous, growable, templated on the element type. C++ wins on raw performance because the compiler monomorphises the element type, but Python wins on flexibility to mix types in one list.
Performance notes for large sequences
Two habits keep list-heavy code fast. Prefer list.append in a loop over repeated concatenation with +, since the latter is O(n^2) and the former is amortised O(1). When you only need to iterate once, consider itertools. itertools.islice lets you walk a slice of a giant sequence without materialising it in memory. Combine these with sorted and reversed, both of which return iterators and avoid a full copy.
Summary
Use lists for changeable collections and tuples for fixed records. Remember to copy when you need a new list, and avoid the multiplication-with-mutable-elements trap. The next lesson covers dicts and sets, the two hash-table-backed collection types in Python.