HSHimanshu Sharma
Python Notes

Python lists: indexing, methods, sorting, copying and comprehensions

Python lists: indexing, methods, sorting, copying and comprehensions Advertisement

The list is the workhorse of Python. It holds an ordered collection of items, lets you add, remove and change them, and can hold values of any type, even mixed. If you store your tasks, your marks, the rows of a file or the results of a search, you will almost certainly use a list. This post covers the whole topic in the order you need it: creating, reading, changing, looping, the main methods, sorting, copying, nested lists and comprehensions. All outputs were produced by running the code on Python 3.9.

Creating a list

Use square brackets, or convert something else with list():

nums = [10, 20, 30, 40]
mixed = [1, "two", 3.0, [4, 5], None]
empty = []
print(nums, len(mixed), empty)           # [10, 20, 30, 40] 5 []
print(list("abc"), list(range(4)), list((1, 2)))
# ['a', 'b', 'c'] [0, 1, 2, 3] [1, 2]

A list can hold duplicates and keeps the order you gave. range(4) is not a list by itself; wrap it in list() to see the numbers.

Accessing items

Indexes start at 0, and negative indexes count from the end. Slices use start:stop:step and never include stop:

nums = [10, 20, 30, 40]
print(nums[0], nums[-1], nums[1:3], nums[::-1], nums[::2])
# 10 40 [20, 30] [40, 30, 20, 10] [10, 30]

An index outside the list raises IndexError; a slice outside the list just gives a shorter result:

try:
    nums[10]
except IndexError as e:
    print(e)                # list index out of range
print(nums[2:100])          # [30, 40]

Lists are mutable

Unlike strings and tuples, a list can be changed in place:

nums = [10, 20, 30, 40]
nums[0] = 99
nums[1:3] = [1, 2, 3]       # replace a slice, even with a different length
print(nums)                  # [99, 1, 2, 3, 40]
del nums[0]
print(nums)                  # [1, 2, 3, 40]

"Mutable" is the key idea behind most list surprises later in this post: when a list changes, every name that refers to it sees the change.

Looping over a list

for n in [10, 20, 30]:
    print(n, end=" ")                       # 10 20 30
print()
for i, n in enumerate(["a", "b"], start=1):
    print(i, n)                              # 1 a  then  2 b
for a, b in zip([1, 2, 3], "xyz"):
    print(a, b, end="; ")                    # 1 x; 2 y; 3 z;
print()

Use enumerate when you need the position, zip when you walk two lists together, and avoid for i in range(len(x)) unless you really need the index. Do not add or remove items from a list while looping over it; build a new list instead (the comprehension section shows how).

The important methods

x = [3, 1, 2]
x.append(4)           # add one item at the end
x.extend([5, 6])      # add every item from another iterable
x.insert(1, 99)       # insert at position 1
print(x)              # [3, 99, 1, 2, 4, 5, 6]

append versus insert

append(item) always adds at the end and is fast. insert(index, item) adds at any position and shifts the later items, which is slower for long lists. Note that append([5, 6]) would add the list as one item, while extend([5, 6]) adds the two numbers.

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

remove versus pop versus del

Method Removes If missing
x.remove(value) the first item equal to value ValueError
x.pop() / x.pop(i) the item at the end / at index i, and returns it IndexError
del x[i] or del x[a:b] the item or slice IndexError for a single bad index
x.clear() everything never
x = [3, 99, 1, 2, 4, 5, 6]
x.remove(99)
print(x.pop(), x.pop(0), x)       # 6 3 [1, 2, 4, 5]
try:
    x.remove(100)
except ValueError as e:
    print(e)                       # list.remove(x): x not in list

Use remove when you know the value, pop when you know the position or want the value back (stacks use append and pop).

Counting and searching

x = [5, 1, 5, 2]
print(x.count(5), x.index(5), x.index(5, 1), len(x), min(x), max(x), sum(x))
# 2 0 2 4 1 5 13
print(5 in x, 7 not in x)         # True True

index(value) raises ValueError if the value is absent, so test with in first when it might be missing.

Sorting and reversing

list.sort() sorts in place and returns None; sorted(x) returns a new sorted list and leaves the original alone.

x = [3, 1, 2]
print(sorted(x), x)                 # [1, 2, 3] [3, 1, 2]
x.sort(reverse=True)
print(x)                            # [3, 2, 1]
x.reverse()
print(x)                            # [1, 2, 3]
words = ["banana", "Fig", "cherry", "kiwi"]
print(sorted(words), sorted(words, key=str.lower), sorted(words, key=len))
# ['Fig', 'banana', 'cherry', 'kiwi'] ['banana', 'cherry', 'Fig', 'kiwi'] ['Fig', 'kiwi', 'banana', 'cherry']

The classic beginner bug is writing x = x.sort(), which sets x to None. The key= argument gives a function that produces the value to sort by. With a mix of upper and lower case, plain sorting puts all capitals first because it compares character codes:

print(sorted(["b", "B", "a"]), sorted(["b", "B", "a"], key=str.lower))
# ['B', 'a', 'b'] ['a', 'b', 'B']

Here key=str.lower ignores the case when comparing, and the stable sort keeps b before B because they were in that order. Sorting is also stable: items that compare equal keep their original order, which lets you sort by several fields by sorting twice, or use a tuple key: key=lambda p: (p["city"], p["name"]). Mixing types that can't be compared ([1, "a"]) raises TypeError.

Operators on lists

a, b = [1, 2], [3]
print(a + b, a * 2, [0] * 3, 2 in a, a == [1, 2], a < [1, 3])
# [1, 2, 3] [1, 2, 1, 2] [0, 0, 0] True True True

+ joins and returns a new list, * repeats, and comparisons go item by item from the left.

Aliasing and cloning

Assigning a list to another name does not copy it. Both names point to the same list:

a = [1, 2, 3]
b = a                  # alias: same object
b.append(4)
print(a, a is b)       # [1, 2, 3, 4] True

To get an independent copy, clone it:

a = [1, 2, 3]
c = a.copy()           # also a[:] or list(a)
c.append(99)
print(a, c, a is c)    # [1, 2, 3] [1, 2, 3, 99] False

This is a shallow copy: it copies the outer list, but nested lists are still shared.

import copy
m = [[1, 2], [3, 4]]
shallow = m.copy()
deep = copy.deepcopy(m)
m[0].append(9)
print(shallow, deep)       # [[1, 2, 9], [3, 4]] [[1, 2], [3, 4]]

Use copy.deepcopy when you need nested lists to be fully independent. A function that receives a list can also change the caller's list, because it gets the same object; copy first if you must not modify it.

Nested lists

A list of lists is how you store a table or a grid:

grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
print(grid[1][2], [row[0] for row in grid], sum(len(r) for r in grid))
# 6 [1, 4, 7] 9

There is one famous trap. [[0] * 3] * 3 does not make three rows; it makes three references to the same row:

bad = [[0] * 3] * 3
bad[0][0] = 1
print(bad)                         # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
good = [[0] * 3 for _ in range(3)]
good[0][0] = 1
print(good)                        # [[1, 0, 0], [0, 0, 0], [0, 0, 0]]

List comprehensions

A comprehension builds a list from a loop in one expression: [expression for item in iterable if condition].

print([n * n for n in range(1, 6)])                       # [1, 4, 9, 16, 25]
print([n for n in range(10) if n % 2 == 0])               # [0, 2, 4, 6, 8]
print(["even" if n % 2 == 0 else "odd" for n in range(4)])   # ['even', 'odd', 'even', 'odd']
print([(a, b) for a in "xy" for b in (1, 2)])
# [('x', 1), ('x', 2), ('y', 1), ('y', 2)]
print([c for c in "Hello World" if c.isupper()])          # ['H', 'W']

Comprehensions are shorter and usually faster than the equivalent loop with append. The if at the end filters; an if ... else at the start chooses a value. When a comprehension needs more than one condition or becomes hard to read, use a normal loop. Replace the square brackets with curly ones for a set or dictionary comprehension, and with round ones for a generator, as the tuple post explains.

Lists as stacks and queues

A stack (last in, first out) is append and pop. A queue (first in, first out) should not use pop(0) on a long list, because every item has to shift; use collections.deque.

stack = []
stack.append(1); stack.append(2)
print(stack.pop())                      # 2
from collections import deque
q = deque([1, 2, 3])
q.append(4)
print(q.popleft(), list(q))             # 1 [2, 3, 4]

Common mistakes

  • Writing x = x.sort() or x = x.append(1); both methods return None.
  • Thinking b = a makes a copy.
  • Using [[0] * 3] * 3 for a grid.
  • Removing items while looping over the same list.
  • Using index() or remove() without checking that the value exists.
  • Using a list when you need fast membership tests; a set is much faster for in.
  • Putting a mutable list in a default argument: def f(x=[]) shares one list between calls.

Practice

Read a line of numbers separated by spaces, convert them into a list of integers, then print the largest, the smallest, the sum, the sorted list and the list without duplicates (keep the first occurrence). Then build a 3 by 3 multiplication table as a nested list with a comprehension and print each row.

Continue

Compare lists with tuples, sets and dictionaries. The functions post uses map, filter and lambda with lists, and the Python cheat sheet puts the list methods on one page. For a guided path, follow the Python video course.

Advertisement