Indexing, Slicing, and Sequence Rules
Key Takeaways
- Python sequence indexes start at 0, so the last valid positive index is len(sequence) - 1.
- Negative indexes count from the right, with -1 selecting the last element and -2 selecting the element before it.
- A slice uses start, stop, and optional step, and the stop index is excluded every time.
- Indexing outside a sequence raises IndexError, but slicing can safely use boundaries beyond the sequence length.
- Slicing a list or tuple creates a new collection object, while slicing a string creates a new string value.
Why this section is worth points
The Data Collections domain (tuples, dictionaries, lists, and strings) is worth 25% of the PCEP-30-02 exam, which is roughly 7 to 8 of the 30 questions. Many of those items are short code-tracing snippets where you must print the exact result. With only 40 minutes for 30 questions, you have about 80 seconds per item, so the indexing and slicing rules below must be automatic.
Sequence positions
A sequence is an ordered collection that Python can address by position. On PCEP the most important sequences are strings, lists, and tuples. They store different data and follow different mutability rules, but they all support len(), indexing, slicing, iteration, and membership tests with in.
The first element is at index 0. If a sequence has four elements, its valid positive indexes are 0, 1, 2, and 3; index 4 is already past the end. Negative indexes count backward from the right side: -1 means the last element, -2 the next-to-last, and so on down to -len(sequence).
items = ['a', 'b', 'c', 'd']
print(items[0]) # a
print(items[-1]) # d
print(items[-3]) # b
print(items[-4]) # a (most-negative valid index)
Slice shape and defaults
A slice has the form sequence[start:stop:step]. The start value is included. The stop value is excluded. That stop exclusion is not a special case; it is the core rule. items[1:3] selects positions 1 and 2, not position 3.
| Expression | Result | Why |
|---|---|---|
items[1:3] | ['b', 'c'] | Start at 1, stop before 3 |
items[:2] | ['a', 'b'] | Default start is the beginning |
items[2:] | ['c', 'd'] | Default stop is the end |
items[:] | ['a','b','c','d'] | Full shallow copy |
items[::2] | ['a', 'c'] | Step by 2 |
items[1::2] | ['b', 'd'] | Start at 1, step by 2 |
items[::-1] | ['d','c','b','a'] | Negative step walks backward |
When step is negative, Python iterates right to left, and the default start becomes the end of the sequence while the default stop becomes the beginning. That is why items[::-1] reverses the whole sequence. A frequent trap: items[3:0:-1] yields ['d','c','b'] because index 0 (the stop) is still excluded.
For code-tracing questions, write the indexes above the values. That makes stop exclusion visible and prevents off-by-one guessing.
Index errors versus slice tolerance
Indexing asks for one position. If that position does not exist, Python raises IndexError.
letters = ['p', 'y']
print(letters[2]) # IndexError
Slicing asks for a range, and Python silently clamps range boundaries that fall outside the sequence. So letters[0:10] returns the available elements instead of raising an error, and letters[5:9] returns an empty list [] rather than an error. This difference between index intolerance and slice tolerance is one of the most-tested traps in this domain.
letters = ['p', 'y']
print(letters[0:10]) # ['p', 'y']
print(letters[5:9]) # []
What slicing returns
A slice returns a new object containing the selected elements. For a list the result is a new list; for a tuple a new tuple; for a string a new string. So later mutation of the original list does not change a previously taken slice.
numbers = [10, 20, 30]
part = numbers[:2]
part[0] = 99
print(numbers) # [10, 20, 30]
print(part) # [99, 20]
Do not overgeneralize this into deep copying. A slice of a list makes a new outer list, but if the elements are themselves mutable objects, both lists still refer to the same inner objects.
PCEP tracing checklist
When a question shows data[a:b:c], identify these facts before choosing an answer:
- What is the sequence length?
- Are any indexes negative?
- Which positions are included before the excluded stop?
- Does the step skip or reverse positions?
- Is the operation indexing one element (can raise IndexError) or slicing a range (tolerant of boundaries)?
That checklist catches most sequence-boundary questions. It also builds the habit needed for range(start, stop, step), where the stop value is excluded in exactly the same style.
Length, membership, and concatenation across sequences
Because strings, lists, and tuples are all sequences, several operators behave consistently across them, and the PCEP exam likes to reuse one rule on a different type than you expect. The len() function returns the number of elements, never the largest index, so a four-element sequence has len of 4 but a maximum valid index of 3. The in operator tests membership and returns a boolean: 3 in [1, 2, 3] is True, 'x' in 'box' is True, and (1,) in [(1,), (2,)] is True because the comparison checks whole elements.
The + operator concatenates two sequences of the same type into a new sequence, while * repeats a sequence: [0] * 3 yields [0, 0, 0] and 'ab' * 2 yields 'abab'. A trap worth memorizing is that you cannot concatenate across types, so [1] + (2,) raises TypeError rather than silently coercing. Comparison operators such as < and == work element by element in lexicographic order, which is why [1, 2] < [1, 3] is True.
Keeping these shared behaviors in mind lets you answer a string question using a rule you first learned for lists, and vice versa, which is exactly how this 25%-weighted domain spreads a small number of rules across many distinct-looking snippets.
What is the result of this code? nums = [0, 1, 2, 3, 4]; print(nums[1:4])
Which expression selects the last element of a non-empty sequence named data?
What happens when Python evaluates ['x', 'y'][0:10]?