Lists and Tuples
Ordered collections you can change and ones you cannot: indexing, slicing, growing a list, and the aliasing surprise where two names turn out to share one list.
Ordered collections you can change and ones you cannot: indexing, slicing, growing a list, and the aliasing surprise where two names turn out to share one list.
You have four hundred photographs to process, and everything you know so far handles exactly one thing at a time. You could write four hundred variables. Nobody has ever done this twice.
What you need is a single name for many values, which you can add to, look through and hand around as one thing. By the end of this lesson you will have two of them — one you can change and one you cannot — and you will know why the second exists, which is less obvious and more useful than it first appears.
A list holds values in order. Write it with square brackets:
photos = ["dawn.jpg", "tram.jpg", "square.jpg"]
print(len(photos)) # 3A list can hold anything, including a mixture, though in practice you usually keep one kind of thing in one list:
mixed = ["dawn.jpg", 400, 3.75, True, None]An empty list is where most lists start, because you build them up as you go:
processed = []Positions start at zero, exactly as with text:
photos = ["dawn.jpg", "tram.jpg", "square.jpg"]
print(photos[0]) # dawn.jpg
print(photos[2]) # square.jpg
print(photos[-1]) # square.jpg - last, without counting
print(photos[-2]) # tram.jpgAsking for a position that does not exist is an error rather than a silent empty answer, which is the behaviour you want:
print(photos[9])
# IndexError: list index out of rangeSlicing takes a range, on the same "up to but not including" rule as strings:
print(photos[0:2]) # ['dawn.jpg', 'tram.jpg']
print(photos[:2]) # the first two
print(photos[1:]) # everything from position 1
print(photos[-2:]) # the last twoA slice always gives you a new list. This matters, and the next section explains why.
Lists are mutable — you can change one after it exists. This is the main difference from text.
photos[0] = "sunrise.jpg" # replace in place
photos.append("bridge.jpg") # add one to the end
photos.extend(["a.jpg", "b.jpg"])# add several
photos.insert(0, "first.jpg") # add at a position
photos.remove("tram.jpg") # remove by value (the first match)
last = photos.pop() # remove and return the last
second = photos.pop(1) # remove and return position 1
del photos[0] # remove by position
photos.clear() # empty itappend is the one you will use most, because building a list
by adding to it in a loop is one of the most common shapes in
all of programming.
Note what these methods return. append, extend, insert,
remove and sort all change the list and hand back None —
they are not like string methods, which leave the original alone
and return a new value. This produces a specific, memorable
mistake:
Bad — assigning the result of a method that changes the list.
photos = ["tram.jpg", "dawn.jpg"]
photos = photos.append("bridge.jpg")
print(photos) # NoneGood — calling it and keeping the list.
photos = ["tram.jpg", "dawn.jpg"]
photos.append("bridge.jpg")
print(photos) # ['tram.jpg', 'dawn.jpg', 'bridge.jpg']The first version does exactly what you wrote: it appends
successfully, then throws the list away and puts None under
the name instead. The next line that touches photos fails with
something about NoneType, several steps from the actual
mistake. The rule is worth stating flatly — methods that
change a list return None — and it is the opposite of the
string rule from two lessons ago.
You met this briefly in the lesson on names. Now that you have lists, it is worth doing properly, because it is the single most common source of "I didn't change that" bugs.
originals = ["dawn.jpg", "tram.jpg"]
One list exists, with one label tied to it.
backup = originals
This is where the bug is. It looks like a copy and is not — it ties a second label to the same list. Nothing was duplicated.
backup.append("square.jpg")
The list changes. There is only one list, so it changes for both labels.
print(originals) — three items
You never wrote originals on the line that changed it, and
it changed anyway.
To get an actual copy, ask for one:
backup = originals.copy() # clearest
backup = originals[:] # a slice of everything - same effect
backup = list(originals) # also worksNow changing backup leaves originals alone.
Two ways, and the difference is the mutability rule again:
photos.sort() # changes photos, returns None
new_list = sorted(photos) # leaves photos alone, returns a listsort() is a method on the list. sorted() is a built-in
function that works on anything you can loop over, and always
hands back a new list. When in doubt, use sorted() — not
changing things unexpectedly is worth the extra characters.
sizes = [340, 12, 8000, 95]
print(sorted(sizes)) # [12, 95, 340, 8000]
print(sorted(sizes, reverse=True)) # [8000, 340, 95, 12]
names = ["tram.jpg", "Dawn.jpg", "square.jpg"]
print(sorted(names)) # capitals first!
print(sorted(names, key=str.lower)) # alphabetical as a human means itThat key argument says what to sort by, and the capital
letter surprise is why it exists: sorting text compares
character codes, and every capital letter sorts before every
lowercase one.
photos.reverse() # flip in place
print(list(reversed(photos))) # a new list, flippedA tuple is an ordered collection you cannot modify. Round brackets instead of square:
dimensions = (1920, 1080)
print(dimensions[0]) # 1920
print(len(dimensions)) # 2
dimensions[0] = 800
# TypeError: 'tuple' object does not support item assignmentThe obvious question is why anyone would want a collection they cannot change.
A sequence that will grow and shrink.
Every position means the same kind of thing, and there could be more or fewer of them tomorrow.
Hand one to a function and that function can modify it.
One record, and this is all of it.
(1920, 1080) is a size, not a list that happens to hold two
numbers today. The positions mean different things.
Nothing can modify it by accident, and — because it cannot change — it can be a dictionary key, which the next lesson relies on.
Tuples appear whether or not you write them yourself. Returning several values from a function makes one:
def get_dimensions():
return 1920, 1080 # this is a tuple
width, height = get_dimensions()That last line is unpacking: pulling a tuple apart into separate names in one step. It works anywhere:
first, second, third = ["a", "b", "c"]
a, b = b, a # swap, with no temporary variable
first, *rest = [1, 2, 3, 4] # first=1, rest=[2, 3, 4]The swap is a small delight the first time you see it. The
*rest form collects whatever is left over into a list.
A workable rule: reach for a list by default, and a tuple when the collection is a fixed record rather than a sequence.
photos = ["dawn.jpg", "tram.jpg"] # list: will grow
dimensions = (1920, 1080) # tuple: a width and a height
coordinates = (38.7223, -9.1393) # tuple: a place
rgb = (255, 128, 0) # tuple: a colourThe test that usually settles it: if the positions mean different things — width then height, latitude then longitude — it is a tuple. If every position means the same kind of thing and there could be more or fewer of them, it is a list.
MAKING THEM
photos = ["a.jpg", "b.jpg"] list - changeable
size = (1920, 1080) tuple - fixed
empty = [] tuple of one: (5,)
GETTING THINGS OUT
x[0] x[-1] first, last
x[1:3] up to but NOT including 3
x[:2] x[2:] x[-2:] a clean split, from, last two
len(x) how many
item in x is it there
x.index(item) where is it (ValueError if absent)
x.count(item) how many times
CHANGING A LIST - all return None
x.append(item) add one to the end
x.extend([a, b]) add several
x.insert(0, item) add at a position
x.remove(item) remove first matching value
x.pop() x.pop(1) remove and RETURN
del x[0] remove by position
x.clear() empty it
ORDERING
x.sort() in place, returns None
sorted(x) a NEW list <- prefer
sorted(x, reverse=True)
sorted(x, key=str.lower) sort by something else
x.reverse() reversed(x)
COPYING
b = a NOT a copy - one list, two labels
b = a.copy() a copy, one level deep
b = a[:] b = list(a) the same thing
copy.deepcopy(a) all the way down
UNPACKING
w, h = (1920, 1080)
a, b = b, a swap
first, *rest = [1, 2, 3]
THE TWO RULES THAT LOOK ALIKE
string methods return a new string, original untouched
list methods change the list and return NoneYou can now hold many values under one name, reach into them,
build them up, sort them, and copy them without accidentally
sharing them. The None-returning rule and the aliasing trap
are the two that will actually catch you, and both are now
things you have seen deliberately rather than discovered at
midnight.
Next is Dictionaries and Sets, which answer a different question. A list is right when position matters. When you want to look something up by name — this photographer's photo count, this file's size — position is the wrong handle, and a dictionary is the tool.
Before you move on, build a list by appending to it in a loop,
then make a copy the wrong way and prove to yourself that both
names changed. Then fix it with .copy(). Doing that
deliberately takes two minutes and saves you the version of the
same bug that takes two hours.