Beginner · Lesson 22 of 94

Lists in depth

Watch this lesson

View full playlist ↗

Read the lesson

What you'll learn

Concept

You know the basics of lists. This lesson covers the operations you reach for once the data is real: sorting with sort and sorted the way a person would want, taking every second item with a step, and copying with .copy() without accidentally sharing. None of it is hard; all of it is the difference between a list you fight and a list that does what you mean.

There are two ways to sort. items.sort() reorders the list in place and returns None. sorted(items), which you met in the built-in functions lesson, leaves the list alone and returns a new sorted list. Both accept key= and reverse=. The key function is called once per item and its result decides the order, so key=len sorts strings by length and key=str.lower sorts names case-insensitively. To sort by several things at once, return a tuple from the key: key=lambda p: (p["city"], p["name"]) sorts by city, then by name within each city. A lambda is a tiny function written on the spot, "take p, return this"; it gets a full treatment in the functional tools lesson.

Lists in depth: ['pear', 'Fig', 'apple' → ['Fig', 'apple', 'pear' → ['apple', 'Fig', 'pear' → ['Fig', 'pear', 'apple'
Lists in depth · Figure 1 · Open for full size

Python's sort is stable: items that compare equal keep their original order. That gives you a trick worth knowing. To sort by city and then by name, you can sort by name first and then by city, and the names stay ordered inside each city. Most of the time the tuple key is clearer, but stability is why sorting twice works, and why sorting by one field never scrambles the others.

Slices accept a third number, the step. items[::2] takes every second item, items[1::2] the ones in between, and items[::-1] is the idiom for a reversed copy. Slices can also be assigned to: items[1:3] = ["x", "y", "z"] replaces two items with three, growing the list. That is rarer, but it explains why a slice is a view of positions rather than a fixed count of items.

Expression Result on [0, 1, 2, 3, 4, 5] Meaning
items[::2] [0, 2, 4] every second item
items[1::2] [1, 3, 5] every second, starting at 1
items[::-1] [5, 4, 3, 2, 1, 0] reversed copy
items[-2:] [4, 5] last two
items[1:3] = [9] [0, 9, 3, 4, 5] replace a slice (list shrinks)

Copying has one rule and one trap. The rule: = never copies; b = a gives one list two names. a.copy(), a[:], and list(a) all make a real copy. The trap: those copies are shallow. If the list contains other lists, the inner lists are still shared, because the copy contains the same inner objects. For nested data you need copy.deepcopy, which the memory-model lesson explains in full; for now, know the word so the bug has a name when you meet it.

Lists in depth: a = [[1, 2 → 3 → 1, 2 → 3 → b = a.copy()
Lists in depth · Figure 2 · Open for full size

Code

Sorting by one key, by a tuple of keys, and case-insensitively. The square-bracket one-liners that pull out names are list comprehensions, which get their own lesson soon; read each as "a list of this, for every p". sorted leaves the original list untouched, which the last line confirms.

# sorting.py
people = [
    {"name": "ada", "city": "London", "age": 36},
    {"name": "Grace", "city": "New York", "age": 45},
    {"name": "linus", "city": "London", "age": 28},
]

by_age = sorted(people, key=lambda p: p["age"])
print([p["name"] for p in by_age])

by_city_then_name = sorted(people, key=lambda p: (p["city"], p["name"].lower()))
print([(p["city"], p["name"]) for p in by_city_then_name])

print(sorted(["pear", "Fig", "apple"]))
print(sorted(["pear", "Fig", "apple"], key=str.lower))
print([p["name"] for p in people])
Output
['linus', 'ada', 'Grace']
[('London', 'ada'), ('London', 'linus'), ('New York', 'Grace')]
['Fig', 'apple', 'pear']
['apple', 'Fig', 'pear']
['ada', 'Grace', 'linus']

In-place sort and stability. After sorting by score, the two 88s keep the order they had, which is what stable means.

# stable.py
results = [("ada", 88), ("linus", 72), ("grace", 88), ("guido", 95)]

results.sort(key=lambda r: r[1], reverse=True)
print(results)
Output
[('guido', 95), ('ada', 88), ('grace', 88), ('linus', 72)]

Steps, reversal, and slice assignment. The reversed copy does not change the original; the assignment does.

# steps.py
items = [0, 1, 2, 3, 4, 5]

print(items[::2], items[1::2])
print(items[::-1])
print(items)
items[1:3] = [9]
print(items)
Output
[0, 2, 4] [1, 3, 5]
[5, 4, 3, 2, 1, 0]
[0, 1, 2, 3, 4, 5]
[0, 9, 3, 4, 5]

Copying, and the shallow-copy trap. Changing the copy's outer list leaves the original alone; changing an inner list changes both, because the inner list is shared.

# copies.py
a = [[1, 2], [3]]
b = a.copy()

b.append([4])
print("a:", a)
print("b:", b)

b[0].append(99)
print("a:", a)
print("b:", b)
Output
a: [[1, 2], [3]]
b: [[1, 2], [3], [4]]
a: [[1, 2, 99], [3]]
b: [[1, 2, 99], [3], [4]]

The small methods that save a loop: extend adds many items, index finds a position, count counts a value, and clear empties.

# more_methods.py
tags = ["python", "web"]
tags.extend(["data", "python"])

print(tags)
print(tags.index("web"), tags.count("python"))
tags.clear()
print(tags)
Output
['python', 'web', 'data', 'python']
1 2
[]

Try it yourself

Create leaderboard.py with a list of at least six (name, score, seconds) tuples. Print the names sorted by score descending and, for equal scores, by seconds ascending, using one tuple key. Then print every second entry of the sorted list, then the list reversed. Finally, copy the list, change the copy, and show the original is intact.

Common mistakes

What's next

Lists are fully in hand. Next you will meet their fixed-size cousin, the tuple, and the collection that cares about uniqueness rather than order: the set.

Course overview