Technology  /  Python

🐍 Python 78 guides · updated 2026

From first variable to OOP, generators, and real projects — the language that runs everything from data pipelines to AI agents, taught the practical way.

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 range

Negative 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 length

Slicing

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 4
print(numbers[6:]) # [6, 7, 8, 9] — from index 6 to end
print(numbers[:]) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] — entire sequence

The 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 element
print(numbers[1::2]) # [1, 3, 5, 7, 9] — every other, starting from 1
print(numbers[::3]) # [0, 3, 6, 9] — every third
print(numbers[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] — reversed
print(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]) # H
print(text[7:12]) # World
print(text[-1]) # !
print(text[:5]) # Hello
print(text[7:]) # World!
print(text[::-1]) # !dlroW ,olleH
# Practical: extract a file extension
filename = "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]) # IndexError
print(items[1:10]) # [2, 3] — stops at end of list, no error
print(items[-10:]) # [1, 2, 3] — starts from beginning, no error

This 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] everywhere
WORD_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 copy
copy.append(99)
print(original) # [1, 2, 3, 4, 5] — unchanged
# Equivalent to
copy = 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 row
print(matrix[:, 1]) # [2 5 8] — second column
print(matrix[0:2, 1:3]) # [[2 3], [5 6]] — submatrix

NumPy 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")) # True
print(is_palindrome("A man a plan a canal Panama")) # True

Chunk a list into fixed-size pieces

data = list(range(10))
chunk_size = 3
chunks = [data[i:i+chunk_size] for i in range(0, len(data), chunk_size)]
# [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]