Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

Python Slicing & Indexing

Master slicing and indexing sequences in Python. This lesson covers core concepts, practical steps, common errors, and what to learn next.

Focus: Python slicing and indexing

Sponsored

Ever tried to grab a substring from a string or pull every other element from a list, only to stare at confusing syntax or get an unexpected empty output? Python's slicing and indexing is one of the most elegant features of the language — abstract from most other languages — yet it trips up beginners and pros alike. Mastering the [start:stop:step] notation will save you hours of manual loops and make your data manipulation code both concise and incredibly readable.

The problem this lesson solves

Without a solid grasp of slicing and indexing, you end up writing verbose, error-prone loops to extract or copy parts of sequences (lists, tuples, strings). You might accidentally mutate your original data, misread off-by-one errors, or give up entirely and use inefficient alternatives like itertools.islice. This lesson transforms that frustration into a superpower: extracting exact subsets of data in a single, declarative line.

Core concept / mental model

Think of a Python sequence as a row of numbered lockers. The indices are the locker numbers, starting at 0. Slicing is like opening a range of consecutive lockers — you specify the first and the one after the last. The step tells you whether to skip lockers, reverse the order, or jump backward.

Key terminology

  • Indexing: Access a single element via its position: seq[0] gets the first element, seq[-1] gets the last.
  • Slicing: Extract a subsequence: seq[start:stop:step].

A slice is a shallow copy for mutable sequences like lists — it creates a new list. For immutable sequences (strings, tuples), it returns a new sequence of the same type.

How it works step by step

Step 1: Basic indexing

data = [10, 20, 30, 40, 50]
print(data[0])   # 10
print(data[-1])  # 50
print(data[-2])  # 40
  • Positive indices: 0, 1, 2, ... from the left.
  • Negative indices: -1, -2, -3, ... from the right.

Step 2: The slice syntax anatomy

sequence[start:stop:step]

  • start: index to begin (inclusive). Default = 0.
  • stop: index to end (exclusive). Default = length.
  • step: how many indices to jump. Default = 1. Negative step means reverse.

Pro tip: The stop index is exclusive. This aligns perfectly with range() and len(): seq[:len(seq)] gives the whole thing.

Step 3: Common slice patterns

items = ['a', 'b', 'c', 'd', 'e']
print(items[1:3])     # ['b', 'c']
print(items[:3])      # ['a', 'b', 'c']
print(items[3:])      # ['d', 'e']
print(items[::2])     # ['a', 'c', 'e']
print(items[::-1])    # ['e', 'd', 'c', 'b', 'a']

Hands-on walkthrough

Let's apply slicing to classic problems.

Example 1: Reversing a string

text = "Python"
rev = text[::-1]
print(rev)  # 'nohtyP'

[::-1] means start at the end, step backward by 1.

Example 2: Extract every other element from a list

scores = [85, 90, 78, 92, 88, 95]
alternate = scores[::2]
print(alternate)  # [85, 78, 88]

This is useful for downsampling data or creating training/test splits.

Example 3: Copy vs. alias — cheap shallow copy

original = [1, 2, 3]
copy_via_slice = original[:]
copy_via_slice[0] = 99
print(original)       # [1, 2, 3]  -> untouched
print(copy_via_slice) # [99, 2, 3]

# Aliasing with = does NOT copy
alias = original
alias[0] = 99
print(original)       # [99, 2, 3]  -> both changed!

Blockquote pro tip: Always use original[:] to make a shallow copy of a list. It's faster and more Pythonic than list(original).

Example 4: Slice assignment (modify in place)

Slicing can also replace a portion of a list without loops:

nums = [1, 2, 3, 4, 5]
nums[1:3] = [99, 100]
print(nums)  # [1, 99, 100, 4, 5]

This works only for mutable sequences (list, bytearray).

Compare options / when to choose what

Feature Slicing itertools.islice Manual loop
Syntactic overhead Very low Medium (needs import) High
Works on all sequences Yes Yes (but not on strings by default) Yes
Supports negative step Yes No (only positive step) You must implement logic
Memory usage Creates new object (copy) Iterator — memory efficient Append to new list — copy
Use case Quick extraction, reverse, copy Large sequences where you don't need a full list When logic is complex (e.g., skip based on value)

When to choose slicing: Always as your first tool for simple positional extraction. Use islice when processing massive iterators (e.g., reading a file line by line) to avoid loading everything into memory.

Troubleshooting & edge cases

Mistake 1: Off-by-one with stop index

items = [1, 2, 3]
print(items[0:2])  # [1, 2]  — items[2] is 3, not included!

Fix: Remember stop is exclusive. Write items[0:3] to get all three.

Mistake 2: Empty slice from reversed step

print(items[2:0:-1])  # [3, 2]

The stop index is still exclusive when stepping backward. items[2:0:-1] goes from index 2 down to index 1 (stops before 0).

Mistake 3: Confusing slice with assignment

matrix = [[1,2],[3,4]]
row_copy = matrix[0]   # This is a reference to the inner list, NOT a copy!
row_copy[0] = 99
print(matrix)          # [[99,2],[3,4]]

Fix: For deep copies of nested lists, use copy.deepcopy(matrix).

Edge case: String immutability

t = "hello"
t[0:2] = "xy"  # TypeError: 'str' object does not support item assignment

Strings are immutable. Use concatenation or str.replace.

What you learned & what's next

You can now slice and index any sequence like a pro: extract with [start:stop:step], reverse with [::-1], and copy with [:]. You know the difference between positive and negative indices, how stop is exclusive, and when slicing beats loops or itertools.islice. You also learned to watch for shallow copy pitfalls and immutable sequence restrictions.

Next: Master list comprehensions — they build on slice-like logic and let you filter and transform sequences in one expressive line.

Practice recap

Try this: Write a function that takes a list of numbers and returns a new list containing every second element in reverse order — in one line. Use slicing only. Bonus: handle empty input gracefully by checking if not lst: before slicing.

Common mistakes

  • Assuming stop is inclusive — always remember seq[0:2] gives only indices 0 and 1.
  • Using [::-1] inside a loop instead of storing the reversed sequence once — performance hit on large data.
  • Accidentally mutating the original list because you used = instead of [:] to copy.

Variations

  1. Use itertools.islice for memory-efficient slicing on large iterators instead of creating a full list.
  2. Use numpy array slicing for high-performance numerical data — it uses a very similar syntax but returns views, not copies.
  3. Use step with negative numbers for patterns like seq[::-2] to reverse and skip elements.

Real-world use cases

  • Extracting the last 10 lines of a log file to find recent errors: log[::-1][:10].
  • Splitting a dataset into training, validation, and test sets with data[:n_train], data[n_train: n_train+n_val], etc.
  • Reversing a string to check for palindromes in a word game without manual loops.

Key takeaways

  • Indexing starts at 0 and uses negative indices to count from the end.
  • Slice syntax [start:stop:step] returns a new sequence; stop is exclusive.
  • Use [:] to create a shallow copy of a list — not assignment.
  • Negative step with [::-1] reverses any sequence in one line.
  • Slice assignment works on mutable lists to replace ranges in-place.
  • For immutables like strings, slicing returns a new string; assignment is disallowed.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.