Python Slice Notation: Syntax and Usage
python slice notation: Learn how Python slice notation works for lists, strings, and other sequences, including step values, negative indices, and slice assignment.
Python slice notation is a compact way to extract a portion of a sequence, such as a list, tuple, or string. The syntax sequence[start:stop:step] looks simple, but the behavior of each part depends on several rules that are easy to misunderstand. This article explains how slice notation actually works, how to use it effectively, and where it commonly breaks.
Slice Notation Basics
The core form of a slice is sequence[start:stop], which returns elements from index start up to but not including index stop. Both start and stop are optional, and if omitted they default to the beginning and the end of the sequence respectively.
data = [10, 20, 30, 40, 50] print(data[1:3]) # [20, 30] print(data[:2]) # [10, 20] print(data[3:]) # [40, 50] print(data[:]) # [10, 20, 30, 40, 50]
The [:] form creates a shallow copy of the entire sequence. For lists, this is a common way to duplicate a list without mutating the original. For strings and tuples, slicing always returns a new object, but the behavior is the same.
When start or stop is negative, it is counted from the end of the sequence. The last element has index -1, the second-to-last -2, and so on. This makes it easy to take the last few elements of a sequence without knowing its length.
data = [10, 20, 30, 40, 50] print(data[-2:]) # [40, 50] print(data[:-1]) # [10, 20, 30, 40] print(data[-3:-1]) # [30, 40]
Notice that stop is still exclusive. data[-3:-1] returns elements from index -3 up to but not including -1, which gives positions -3 and -2. This is consistent with the normal slice rule.
Negative Indices and Reversed Slicing
Negative indices are not just a convenience; they also affect how the slice is interpreted when combined with a step. The step parameter controls the direction and stride of the slice. A positive step moves forward, while a negative step moves backward.
data = [10, 20, 30, 40, 50] print(data[::-1]) # [50, 40, 30, 20, 10] print(data[4:0:-2]) # [50, 30] print(data[-1:-6:-1]) # [50, 40, 30, 20, 10]
When step is negative, the default values for start and stop change. If start is omitted, it defaults to the end of the sequence. If stop is omitted, it defaults to the beginning. This is why data[::-1] reverses the list. The slice starts at the last element and moves backward until the beginning.
It is important to remember that the stop index is still exclusive even with a negative step. In data[4:0:-2], the slice starts at index 4 and goes down to but not including index 0, so it includes indices 4 and 2. If you need to include index 0, you must omit stop or use None.
Step Values and Their Effects
The step value can be any non-zero integer. A step of 1 is the default and returns every element in the range. A step of 2 returns every second element, and so on. A step of -1 reverses the sequence, as shown above.
data = [0, 1, 2, 3, 4, 5] print(data[::2]) # [0, 2, 4] print(data[1::2]) # [1, 3, 5] print(data[5:0:-2]) # [5, 3, 1]
A step of 0 raises a ValueError because it would cause an infinite loop. Python explicitly rejects this case rather than silently misbehaving.
When the step is negative, the start and stop indices must be interpreted relative to the direction of traversal. A common mistake is to use data[0:-1:-1] expecting a reversed slice, but this returns an empty list because start is before stop when moving backward. To reverse a list, use data[::-1] or data[-1::-1].
Slice Assignment and Deletion
Slice notation is not limited to reading values. You can assign to a slice, which replaces the selected elements with the elements from another iterable. The length of the replacement does not need to match the length of the slice, so assignment can change the size of the list.
data = [1, 2, 3, 4, 5] data[1:3] = [20, 30, 40] print(data) # [1, 20, 30, 40, 4, 5] data[1:2] = [100, 200] print(data) # [1, 100, 200, 30, 40, 4, 5]
You can also delete a slice with the del statement, which removes the selected elements without replacing them.
data = [1, 2, 3, 4, 5] del data[1:3] print(data) # [1, 4, 5]
Slice assignment works on lists, but not on immutable sequences like strings or tuples. Attempting to assign to a string slice raises a TypeError. This is an important distinction when you are deciding whether to use a list or a string for a particular task.
Slice Objects and Reusability
Slice notation can be used directly in brackets, but it can also be stored as a slice object and reused. The slice() built-in function creates a slice object with start, stop, and step attributes.
s = slice(1, 5, 2) data = [10, 20, 30, 40, 50, 60] print(data[s]) # [20, 40]
This is useful when the same slice is applied to multiple sequences or when the slice boundaries are computed dynamically. For example, you might define a slice that selects every other element from a dataset and apply it to several lists.
Slice objects also appear in custom classes that implement the sequence protocol. When you define __getitem__ to accept a slice, you can control how your class responds to slice notation. This is how libraries like NumPy extend slicing to multi-dimensional arrays, but even in pure Python, understanding slice objects helps you build more expressive APIs.
Common Mistakes and Edge Cases
Several edge cases in slice notation often surprise developers. One is that out-of-range indices do not raise an error. If start or stop is larger than the sequence length, the slice simply returns what is available.
data = [1, 2, 3] print(data[1:100]) # [2, 3] print(data[-100:2]) # [1, 2]
This behavior is intentional and often useful, but it can hide bugs when you expect an index error. If you need to validate that a slice is within bounds, you must do it explicitly.
Another common mistake is confusing the order of start and stop when using a negative step. For example, data[0:3:-1] returns an empty list because you cannot move backward from index 0 to index 3. The slice is empty, not an error. This is a frequent source of confusion in code that attempts to reverse a portion of a sequence.
Finally, remember that slicing a list creates a new list. If you are working with large lists, slicing can consume significant memory. For read-only access to a subrange, you might consider using itertools.islice to avoid copying, but that only works with iterables, not with direct indexing.
Performance and Memory Considerations
Slice notation is implemented in C and is generally fast, but it always creates a new object for the result. For lists, this means a shallow copy of the selected elements. The time and memory cost are proportional to the length of the slice, not the length of the original list. This is usually fine, but it matters when you repeatedly slice a large list in a loop.
# This copies the entire list each iteration for i in range(1000): chunk = data[i:i+10]
If you only need to iterate over a slice without storing it, consider using itertools.islice to avoid the copy. However, islice does not support negative indices or steps, so it is not a drop-in replacement.
For strings, slicing also creates a new string, which can be expensive if you are building a large string from many slices. In such cases, it is often better to collect pieces in a list and join them at the end.
Slice assignment, on the other hand, can be more efficient than deleting and inserting elements separately because it is a single operation that shifts the remaining elements only once. This is a practical consideration when you are modifying lists in performance-sensitive code.
Understanding these tradeoffs helps you choose between slicing and other approaches like list.copy(), itertools.islice, or explicit loops. The right choice depends on whether you need a new object, whether you can iterate lazily, and how large the data is.