Python Indexing and Slicing: Accessing Sequences the Pythonic Way
Indexing and slicing are how you extract values from sequences — lists, strings, tuples, and more. The syntax is compact and expressive, but the rules around stop values being exclusive and negative indices counting from the end cause confusion until they click. Once they do, you’ll use slicing constantly.
Indexing
Python uses zero-based indexing: the first element is at index 0, the second at 1, and so on.
fruits = ["apple", "banana", "cherry", "date", "elderberry"]# 0 1 2 3 4
print(fruits[0]) # "apple"print(fruits[2]) # "cherry"print(fruits[4]) # "elderberry"Accessing an index outside the valid range raises an IndexError:
print(fruits[5]) # IndexError: list index out of rangeNegative Indexing
Negative indices count from the end of the sequence. Index -1 is the last element, -2 is second to last, and so on:
fruits = ["apple", "banana", "cherry", "date", "elderberry"]# -5 -4 -3 -2 -1
print(fruits[-1]) # "elderberry"print(fruits[-2]) # "date"print(fruits[-5]) # "apple"This is especially useful when you need the last element without knowing the length:
lines = open("data.txt").readlines()last_line = lines[-1] # works regardless of file lengthSlicing
Slicing extracts a sub-sequence. The syntax is sequence[start:stop:step].
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[2:5]) # [2, 3, 4] — indices 2, 3, 4 (stop is exclusive)print(numbers[:4]) # [0, 1, 2, 3] — from start to index 4print(numbers[6:]) # [6, 7, 8, 9] — from index 6 to endprint(numbers[:]) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] — entire sequenceThe stop index is always exclusive. numbers[2:5] gives you indices 2, 3, 4 — not 5. This means numbers[2:5] has 5 - 2 = 3 elements.
Step values
The step controls which elements are included:
print(numbers[::2]) # [0, 2, 4, 6, 8] — every other elementprint(numbers[1::2]) # [1, 3, 5, 7, 9] — every other, starting from 1print(numbers[::3]) # [0, 3, 6, 9] — every thirdprint(numbers[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] — reversedprint(numbers[8:2:-1]) # [8, 7, 6, 5, 4, 3] — reverse with specific range[::-1] is the idiomatic way to reverse a sequence in Python.
Slicing Strings
All the same rules apply to strings:
text = "Hello, World!"
print(text[0]) # Hprint(text[7:12]) # Worldprint(text[-1]) # !print(text[:5]) # Helloprint(text[7:]) # World!print(text[::-1]) # !dlroW ,olleH
# Practical: extract a file extensionfilename = "report_2025.pdf"extension = filename[-3:] # "pdf"year = filename[7:11] # "2025"Slicing Tuples
Tuples slice the same way as lists and strings. The result is always a tuple:
coords = (10, 20, 30, 40, 50)print(coords[1:4]) # (20, 30, 40)print(coords[::-1]) # (50, 40, 30, 20, 10)What Happens at Boundaries
Unlike indexing, slicing never raises an error for out-of-bounds values — it just returns what’s available:
items = [1, 2, 3]
print(items[10]) # IndexErrorprint(items[1:10]) # [2, 3] — stops at end of list, no errorprint(items[-10:]) # [1, 2, 3] — starts from beginning, no errorThis makes slicing safe to use without bounds checking.
The slice() Object
Python’s slice() function creates a reusable slice object. This is useful when you have the same slice applied to multiple sequences, or when you want to name slices for clarity:
# Instead of repeating [7:12] everywhereWORD_SLICE = slice(7, 12)
texts = ["Hello, World!", "Hello, Alice!", "Hello, Bob!!!"]for text in texts: print(text[WORD_SLICE]) # World, Alice, Bob!!
# Named slices for structured data (like fixed-width CSV records)FIRST_NAME = slice(0, 10)LAST_NAME = slice(10, 20)AGE = slice(20, 23)
record = "Alice Smith 030"print(record[FIRST_NAME].strip()) # "Alice"print(record[LAST_NAME].strip()) # "Smith"print(record[AGE].strip()) # "30"Slicing and Copying Lists
Slicing a list returns a shallow copy. This is a common idiom for copying:
original = [1, 2, 3, 4, 5]copy = original[:] # shallow copycopy.append(99)print(original) # [1, 2, 3, 4, 5] — unchanged
# Equivalent tocopy = original.copy()Note that this is a shallow copy — nested mutable objects are still shared. Use copy.deepcopy() for fully independent copies of nested structures.
NumPy Arrays
NumPy extends slicing to multi-dimensional arrays. The syntax is similar but more powerful:
import numpy as np
matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(matrix[0]) # [1 2 3] — first rowprint(matrix[:, 1]) # [2 5 8] — second columnprint(matrix[0:2, 1:3]) # [[2 3], [5 6]] — submatrixNumPy slices return views into the original array, not copies — modifying a slice modifies the original. This is intentionally different from Python list slicing and is a frequent source of confusion when switching between the two.
Common Patterns
Last n elements
items = list(range(20))last_five = items[-5:] # [15, 16, 17, 18, 19]All but the first element
all_but_first = items[1:]Remove first and last
trimmed = items[1:-1]Check if a string is a palindrome
def is_palindrome(s): s = s.lower().replace(" ", "") return s == s[::-1]
print(is_palindrome("racecar")) # Trueprint(is_palindrome("A man a plan a canal Panama")) # TrueChunk a list into fixed-size pieces
data = list(range(10))chunk_size = 3chunks = [data[i:i+chunk_size] for i in range(0, len(data), chunk_size)]# [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]