Sequence Containers: Lists and Tuples
So far we've covered basic object types in Python, such as strings and numbers. Now we're ready to look at Python's "container" objects, starting with lists and tuples. Both lists and tuples are sequence types. Because strings are sequence types as well, much of what we learn here applies to strings too.
Sequence types have a specific order, so it's easy to identify a string's first and last characters. Similarly, lists and tuples present elements in a particular order. Each element of a sequence is numbered, always starting at zero. You refer to an individual element by following the sequence name with a number in square brackets.
Python uses both lists and tuples. In general, tuples are used when the position of an element says something about what it represents, and lists are used to hold elements that will be treated in the same manner. Python doesn't enforce these conventions, though; the only hard rule is don't use tuples if you want to change the sequence. Tuples are for non-modifiable sequences.
Sometimes you'll want to write the contents of a list right inside your code. To do so, write a comma-separated list of element values surrounded by square brackets.
Tuples are usually written as a comma-separated list of values surrounded by parentheses rather than brackets, though in many cases the parentheses are optional. The interactive interpreter always displays a tuple with parentheses, and we recommend you write them the same way to make your code easier to read.
Let's look at some sequences in action:
>>> lst1 = [1, 3, 5] >>> lst2 = [2, 4, 6] >>> tup1 = (9, 7, 5) >>> tup2 = (8, 6, 4) >>> dir(lst1) ['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort'] >>> dir(tup1) ['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index'] >>> lst1+lst2 [1, 3, 5, 2, 4, 6] >>> tup1+tup2 (9, 7, 5, 8, 6, 4) >>> lst1+tup1 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can only concatenate list (not "tuple") to list >>> clist = [lst1, lst2, tup1, tup2] >>> print(clist) [[1, 3, 5], [2, 4, 6], (9, 7, 5), (8, 6, 4)]
The dir() calls show that a list has methods a tuple
doesn't. The public (non-dunder) methods of a list are
append, clear, copy,
count, extend, index,
insert, pop, remove,
reverse, and sort. A tuple has only
count and index. We'll look at those methods
later, but for now we'll focus on the general behaviour of sequences.
You can put whatever you like in a list—usually simple values like
strings and numbers. The clist list above contains two other
lists and two tuples.
| Note | The full output of dir() varies with the
Python version. Current Python reports more dunder methods than the
original 3.1 interpreter did, because the language has grown. The
public methods shown above are what matters for everyday use. |
Once you have created a sequence you can access its individual elements using indexing. To index a single element, follow the sequence with a numeric value in square brackets—remember, the first element is numbered zero. You can also take slices, creating a new and usually smaller sequence. To slice a sequence, write two numeric values separated by a colon inside the square brackets. If you omit the first value the new sequence starts with the first element; if you omit the last value it ends with the last element. Try these:
>>> clist = [1, (2, 3, 4), "a", "Bright", "c"] >>> clist[1] (2, 3, 4) >>> clist[1][1] 3 >>> clist[3][1:3] 'ri' >>> stuff = clist[2:4] >>> stuff ['a', 'Bright'] >>> stuff2 = clist[2:5] >>> stuff2 ['a', 'Bright', 'c'] >>> stuff[0] 'a' >>> "Strings are sequences too"[:7] 'Strings'
Indexing and slicing are fundamental operations in Python, so make sure
you understand why each expression evaluates as it does. Bear in mind
that when you slice a sequence the second index is not the index of the
last element in the slice—it's the index of the first element
excluded. This is actually very useful: clist[2:4]
always gives you a two-element list. To include element four we had to
reference the nonexistent element five. Because strings are sequences
too, you can chop them up in exactly the same way.
Although strings and tuples are sequences, they are immutable. Once created they can't be changed—though you can still index and slice them to extract elements or sub-sequences. Lists, however, can be changed. In the same way that you can bind a new value to a name with an assignment, you can bind a new value to an element of a list:
>>> stuff = [1, (2, 3, 4), "a", "Bright", "c"] >>> stuff[1] = "Not a tuple" >>> stuff [1, 'Not a tuple', 'a', 'Bright', 'c'] >>> stuff[0] = 0 >>> stuff[3] = 'b' >>> stuff [0, 'Not a tuple', 'a', 'b', 'c'] >>> stuff[2:4] ['a', 'b'] >>> stuff[2:4] = [1, 2, 3] >>> stuff [0, 'Not a tuple', 1, 2, 3, 'c']
So far we've just been replacing single elements of the list. It's also possible to replace a slice, as the last few lines show. When you do that, the right-hand side must be a sequence—any sequence will do: a list, tuple, or string. If you assign a string to a slice, each character in the string becomes a new element of the list. Try experimenting with these possibilities.
Because you can replace any slice of a list, you can delete a slice by assigning an empty sequence to it. Python's del statement does the same job more directly. You can use it on a single element or on a slice. If you know a list contains a certain value but don't know its index, the list's remove() method will delete the first occurrence for you. If the same value occurs more than once, only the first is removed. Type the commands below:
>>> dlist = ['a', 'b', 'c', '1', '2', 1, 2, 3] >>> dlist[6] 2 >>> del dlist[6] >>> dlist ['a', 'b', 'c', '1', '2', 1, 3] >>> dlist[:3] ['a', 'b', 'c'] >>> del dlist[:3] >>> dlist ['1', '2', 1, 3] >>> dlist.remove(1) >>> dlist ['1', '2', 3]
| Note | In the last example, element 2 (the integer
1) was removed, not element 0 (the string
'1'). Python doesn't convert between strings and numbers
unless you explicitly ask it to.Also, remember that deletion only works for lists. Deleting an element from a string or tuple would amount to modifying an immutable sequence, which Python won't allow. |
We can add elements to a list using the list's append() method: call it with the new element and it's appended at the end. To insert at a specific position there are two approaches: the list's insert() method, which takes an index and a value; or assigning a value to an empty slice (any slice where the lower and upper indices are equal). Try these:
>>> elist = [] # The empty list
>>> elist.append('a')
>>> elist
['a']
>>> elist.append('b')
>>> elist
['a', 'b']
>>> elist.append((1, 2, 3))
>>> elist
['a', 'b', (1, 2, 3)]
>>> len(elist)
3
>>> elist[1:1]
[]
>>> elist[1:1] = ["new second element"]
>>> elist
['a', 'new second element', 'b', (1, 2, 3)]
>>> elist.insert(3, "4th")
>>> elist
['a', 'new second element', 'b', '4th', (1, 2, 3)]
>>> len(elist)
5
One limitation of slice assignment is that the replacement must be a
sequence, which is why we usually reach for append() or
insert(). If you have a whole sequence of elements to
insert, though, slice assignment can be more concise than any
alternative.
| Note | When you call append() with a sequence
argument—as in elist.append((1, 2, 3)) above—the entire
sequence becomes a single new element at the end of the list. If you
want to add each element of a sequence individually, use
extend() instead. |
A slice specifies a subsequence. Suppose you don't want every element,
but rather every second or third one. The easiest way is to use a third
component of the slice specification: the stride. The stride says
how many positions to advance between each extracted element, and is
separated from the other two components with a colon:
[first:last:stride].
When you omit the stride it defaults to 1, which takes every element. A stride of 2 takes every second element, and so on. Stride values can be negative as well as positive. Slicing starts at the first component, advances by the stride, and stops when the index reaches or passes the second component. When the stride is negative, the first component must be higher than the second. Try these:
>>> alf = "abcdefghijklmnopqrstuvwxyz" >>> alf[2:13] 'cdefghijklm' >>> alf[2:13:2] 'cegikm' >>> alf[2:13:-2] '' >>> alf[13:2:-2] 'nljhfd' >>> alf[13:2] '' >>> alf[::-1] 'zyxwvutsrqponmlkjihgfedcba'
One way to reverse a sequence is to slice the whole thing with a stride
of -1. So to replace a list with its reverse you can write
lst = lst[::-1] rather than calling
lst.reverse(). Python sequences are nothing if not
versatile.
Sometimes you'll have a string that you want to break up into a list of words. The split() method does exactly that. Called without arguments it splits on any whitespace; called with a string argument that string is used as the separator, and the pieces between its occurrences are returned as a list.
If you supply a second argument it should be an integer specifying the maximum number of splits, which limits the number of elements in the returned list.
To get the sum of the numbers in a sequence, pass the sequence to the sum() function. An exception is raised if any non-numeric element is present. To get the length of any sequence, use len().
To count how many times a particular element appears in a list or tuple, use the count() method with the element value as the argument.
To determine whether a sequence contains a specific value, use the in keyword, which returns either True or False. Sequences also have an index() method that returns the lowest index at which a given element occurs. Be careful, though: index() raises an exception if the element isn't present. You can guard against that with an if test first, but it's usually cleaner to handle the exception directly and avoid searching twice. We'll cover the if statement and exception handling in detail later.
Here is a concise program that splits a string into words using the built-in string methods:
"""Simpler program to list the words of a string."""
s = input("Enter your string: ")
words = s.strip().split()
for word in words:
print(word)
Save it as better_sentence_splitter.py. Type in a string that contains some whitespace, press Enter, and you'll see the words printed one per line.
s = input("Enter your string: ") words = s.strip().split() for word in words: print(word)
The strip() method is applied to string s, returning a version with no leading or trailing whitespace. The split() method is then applied to the stripped string, returning a list of words. The for loop iterates over that list, printing each word on a separate line.
Now let's do something more substantial with lists: count the lines, words, and characters in a chunk of text. We measure characters with len(), count lines by splitting on newlines, and accumulate a word total by splitting each line and summing the lengths. Save this as paragraph_stats.py:
"""Count the words, lines and characters in a chunk of text."""
gettysburg = """\
Four score and seven years ago our
fathers brought forth on this continent,
a new nation, conceived in Liberty, and
dedicated to the proposition that
all men are created equal.
Now we are engaged in a great civil war,
testing whether that nation, or
any nation so conceived and so dedicated,
can long endure. We are met on
a great battle-field of that war. We have
come to dedicate a portion of that
field, as a final resting place for those
who here gave their lives that that
nation might live. It is altogether
fitting and proper that we should do this."""
charct = len(gettysburg)
lines = gettysburg.split("\n")
linect = len(lines)
wordct = 0
for line in lines:
words = line.split()
wordct += len(words)
print("The text contains", linect, "lines,", wordct, "words, and", charct, "characters.")
Run it, and you should see:
The text contains 16 lines, 102 words, and 557 characters.
| Note | Some operating systems may give different results: Unix records a newline as one character, while Windows records it as two. |
Now let's extend the program to keep a count of word lengths, so we know how many one-letter, two-letter, three-letter words there are, and so on. Modify paragraph_stats.py as shown:
"""Count the words, lines and characters in a chunk of text."""
gettysburg = """\
Four score and seven years ago our
fathers brought forth on this continent,
a new nation, conceived in Liberty, and
dedicated to the proposition that
all men are created equal.
Now we are engaged in a great civil war,
testing whether that nation, or
any nation so conceived and so dedicated,
can long endure. We are met on
a great battle-field of that war. We have
come to dedicate a portion of that
field, as a final resting place for those
who here gave their lives that that
nation might live. It is altogether
fitting and proper that we should do this."""
lengthct = [0]*20 # a list of 20 zeroes
charct = len(gettysburg)
lines = gettysburg.split("\n")
linect = len(lines)
wordct = 0
for line in lines:
words = line.split()
wordct += len(words)
for word in words:
lengthct[len(word)] += 1
print("The text contains", linect, "lines,", wordct, "words, and", charct, "characters.")
for i, ct in enumerate(lengthct):
if ct:
print("Length", i, ":", ct)
We begin by creating a list of twenty counts, all initialised to zero.
The count of n-letter words will be stored in
lengthct[n]; we assume no word will be longer than
nineteen characters. Within the line-processing loop we've added an
inner loop that iterates over the words: the length of each word is used
as an index into lengthct, and that element is incremented
by one. After processing the text, we print the word-length counts,
omitting any lengths with a zero count.
With the same Gettysburg text, the output should look like this:
The text contains 16 lines, 102 words, and 557 characters. Length 1 : 5 Length 2 : 19 Length 3 : 19 Length 4 : 16 Length 5 : 15 Length 6 : 6 Length 7 : 12 Length 8 : 2 Length 9 : 3 Length 10 : 3 Length 11 : 1 Length 12 : 1
Experiment further. Modify the text so it contains a word of twenty characters or more (like "deinstitutionalizing"). What happens when you run the program? How could you fix it? Can you think of a way to count individual words, to see how many times each one is used? With sequences alone this is possible, though not particularly easy.
| Modern Python | The enumerate(lengthct) call in the final
loop is the idiomatic way to iterate over a sequence while also
tracking the index. It's cleaner than a manual counter like
i = 0; i += 1 or an index-based
range(len(...)) loop. |
| Modern Python | Counting word frequencies—the problem posed at the end
of the section—becomes straightforward with a dict (or
collections.Counter). We'll get to those in the next
lesson. |
You've learned quite a bit about Python's sequence types and just how useful they can be. Next, we'll check out Python's mapping types.
Phew. This isn't easy, but you're doing really well. Keep it up, and I'll see you in the next lesson!
