6. Immutable Sequences#

In this section, we will explore data structures that allow us to store multiple values simultaneously, focusing specifically on two immutable (see glossary Immutable) sequences (see glossary Sequence) : tuple and str.

6.1. Tuple#

A tuple (see glossary Tuple) is an ordered collection of items, often referred to as an “n-tuple” in mathematical terms, where “n” denotes the number of elements. In Python, tuples are created using parentheses with elements separated by commas. For instance, the tuple (1, 4, 'a') consists of three elements: 1, 4, and 'a'. Notably, tuples can contain elements of varying data types. A tuple can be assigned to a variable as shown below:

>>> a = (1, 4, 'a')
>>> print(a)
(1, 4, 'a')

It is also possible to create a tuple with a single element. To differentiate it from a parenthesized single value, Python requires a trailing comma:

>>> a = (1)
>>> print(a)
1
>>> a = (1,)
>>> print(a)
(1,)

6.1.1. Accessing Values#

The elements within a tuple are indexed starting from 0. You can access these elements using square bracket notation: a[index]. For example:

a = (1, 4, 'a')
print(a[0])  # Output: 1
print(a[1])  # Output: 4
print(a[2])  # Output: 'a'

Attempting to access an index that is out of range will result in an IndexError. For instance, trying to access a[3] in a tuple of length 3 will raise an error because indices are zero-based.

a = (1, 4, 'a')
print(a[3])  
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: tuple index out of range

Tuples are immutable, meaning their contents cannot be altered once created. Any attempt to modify an element will result in a TypeError.


a = (1, 4, 'a')
a[0] = 100  
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

Again, these two errors are quite clear, you have to learn to read the error message.

Tuples can also be accessed using negative indices, which count backward from the end of the tuple. For example

a = (1, 4, 'a')
print(a[-1])  # Output: 'a'
print(a[-2])  # Output: 4
print(a[-3])  # Output: 1

It’s important to stay within the valid range of indices. Accessing an out-of-range negative index, such as a[-4] in a tuple of length 3, will also raise an IndexError.

6.1.2. Length#

Python provides the len function to determine the number of elements in a tuple. This function is useful for understanding the boundaries of a tuple.

a = (1, 4, 'a')
length = len(a)
print(length)  # Output: 3

The len function helps establish the valid range for indexing: from 0 to len(a) - 1 for positive indices, and from -1 to -len(a) for negative indices.

6.1.3. Concatenation and scalar multiplication#

While tuples themselves are immutable and cannot be altered, you can concatenate tuples to form a new tuple using the + operator. Here’s how it works.

>>> a = (1, 2)
>>> b = (3, 4)
>>> a + b
(1, 2, 3, 4)

You can also multiply a tuple by a scalar.

>>> a = (1, 2)
>>> a*2
(1, 2, 1, 2)
>>> 2*a
(1, 2, 1, 2)

6.1.4. Comparison#

Tuples can be compared using standard comparison operators to check for equality or order. Python compares tuples element-wise:

>>> print((1, 2) == (1, 2))  
True
>>> print((1, 2) == (1, 2, 3)) 
False
>>> print((1, 2) == (2, 1)) 
False

>>> print((1, 2) < (2, 1))
True
>>> print((1, 2) < (1, 3))
True
>>> print((1, 2) < (0, 3))
False

You can also test for membership within a tuple using the in keyword, which returns a boolean value:

print(1 in (1, 2, 3, 4))  # Output: True
print(0 in (1, 2, 3, 4))  # Output: False
print((1,2) in (1, 2, 3, 4))  # Output: False

6.1.5. Usage of Tuples#

Tuples are useful for grouping multiple values together and treating them as a single entity. They are especially handy when a function needs to return multiple values. By returning a tuple, the function can encapsulate and return multiple pieces of information in a single return statement.

6.2. A str is a sequence#

As tuple, strings are sequences and allow access to individual characters via indexing. For example, given a string variable:

>>> s = "abcd"
>>> s[0]
'a'
>>> s[3]
'd'

In this code, s[0] returns the character at index 0 of the string, which is 'a', and s[3] returns the character at index 3, which is 'd'. This behavior mirrors tuple indexing.

Like tuples, strings in Python are immutable. This means that once a string is created, its contents cannot be changed. Attempting to assign a new value to a specific index of a string will result in an error. You can determine the length of a string using the len function, concatenate strings with + or *, compare them for equality or alphabetic order, and check for the presence of substrings. For instance:

>>> len("This string is too long and " + "concatenated with another")
53
>>> "yes" == "yes"
True
>>> "yes" == "Yes" # case sensitive
False
>>> "Supélec" in "CentraleSupélec"
True

In these examples, len calculates the total number of characters in the concatenated string. String equality checks are case-sensitive, so "yes" and "Yes" are considered different. The in keyword allows checking whether a substring, such as "Supélec", exists within a larger string.

Additionally, it’s important to note that a single character in Python is effectively a string of length one. Thus, accessing an individual character from a string results in a string of one character. For example:

>>> single_char = s[0]  # 'a'
>>> type(single_char)
<class 'str'>

In this context, single_char is a string, even though it contains only one character. This property underscores Python’s consistent treatment of characters and strings, simplifying many text manipulation tasks.

6.3. Number Sequences - range#

In Python, generating sequences of integers is streamlined through the use of the range function. The range function is not a typical sequence, but an object that efficiently produces a sequence of numbers on demand. It is more efficient in term or memory for a large sequence to generate the number one by one. The behavior of range varies depending on the number of parameters provided. When called with a single argument v, range generates a sequence of integers starting from 0 up to, but not including, v.

For example, consider the following code snippet:

>>> r = range(10)
>>> print(r)
range(0, 10)

Here, the output does not display a sequence of numbers but rather the range object itself. This is because range creates a generator, not the sequence of numbers directly. To extract the sequence, the generator can be converted into a tuple.

>>> t = tuple(r)
>>> print(t)
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

This conversion yields a tuple containing the integers from 0 to 9. Like other sequences in Python, range supports operations such as indexing and determining its length using functions like len.

>>> r = range(10)
>>> r[5]
5
>>> len(r)
10

Beyond generating sequences from 0 up to a specified value, range can also generate sequences between two specified values. Moreover, it can generate sequences with a specific step, allowing for more complex number sequences.

>>> tuple(range(3, 10))      # Generates numbers from 3 to 9.
(3, 4, 5, 6, 7, 8, 9)
>>> tuple(range(1, 10, 2))   # Generates odd numbers from 1 to 9 (with a step of 2).
(1, 3, 5, 7, 9)

The flexibility of range even allows for the generation of decreasing sequences by specifying a negative step.

>>> tuple(range(10, 1, -1))
(10, 9, 8, 7, 6, 5, 4, 3, 2) # Generates numbers from 10 down to 2 with step -1.

The range function is a powerful tool in Python, especially when working with loops and iterative operations. It provides a memory-efficient way to generate large sequences without actually storing them in memory, which is a key advantage when dealing with large datasets or extensive computations.

6.4. Slicing in Python#

Slicing (see glossary Slicing) is a fundamental operation in Python when working with sequences, such as strings or tuples. Slicing allows you to extract a subsequence or a subset of elements from a sequence, creating a new sequence of the same type. The basic slicing syntax is sequence[start:stop:step], where start is the index of the first element to include, stop is the index of the first element to exclude, and step determines the stride between elements. Let’s explore these concepts with some examples.

Consider the following string containing all the letters of the alphabet:

c = "abcdefghijklmnopqrstuvwxyz"

6.4.1. Basic Slicing#

To extract the first 10 characters from this string, you specify the start and stop indices separated by a colon.

>>> c[0:10]
'abcdefghij'

Here, the slicing operation starts at index 0 and stops just before index 10. As with the range function, the start index is inclusive, while the stop index is exclusive.

You can also omit either the start or stop index.

>>> c[:2]
'ab'
>>>
c[2:]
'cdefghijklmnopqrstuvwxyz'
  • c[:2] extracts the substring from the beginning of the string up to, but not including, index 2.

  • c[2:] extracts the substring from index 2 to the end of the string.

6.4.2. Slicing with a Step#

The step parameter allows you to specify the stride of the slice, meaning you can choose to skip elements. For example, to extract every second character from the string.

>>> c[::2]
'acegikmoqsuwy'

Here, c[::2] starts from the beginning and includes every second character until the end.

6.4.3. Negative Indices and Reverse Slicing#

Python sequences also support negative indices, which allow you to slice relative to the end of the sequence.

>>> c[-5:]
'vwxyz'

In this example, c[-5:] extracts the last 5 characters of the string. The index -1 refers to the last element, -2 to the second last, and so forth.

You can even reverse a sequence by specifying a negative step.

>>> c[::-1]
'zyxwvutsrqponmlkjihgfedcba'

This operation reverses the entire string by starting from the end and stepping backwards. Slicing and Immutability

Overall, slicing is a versatile and powerful feature in Python that allows for efficient manipulation and extraction of data from sequences. Understanding how to leverage slicing effectively can significantly enhance your ability to work with data in Python.