String Slicing and Indexing in Python

Every character in a Python string sits at a specific position, and python string indexing and python string slicing are how you reach in and grab exactly what you need — a single character, or an entire chunk. You've seen the basics of this already in the earlier strings article; this one goes deeper into the mechanics, the step parameter, and the errors each approach can (and can't) raise.

Introduction: Strings as Sequences of Characters

A string in Python behaves like an ordered sequence of characters, and every character has a numbered position. Indexing lets you grab a single character at a specific position. Slicing lets you extract an entire range — a substring.

Python counts positions two ways at once: positive indices start at 0 and count from the left, while negative indices start at -1 and count from the right.

 P   y   t   h   o   n
 0   1   2   3   4   5
-6  -5  -4  -3  -2  -1

Both indexing systems point at the same characters — they're just two different directions to count from, and Python lets you use whichever is more convenient for a given situation.

Indexing: Accessing Single Characters

Basic syntax
word = "Python"

print(word[0])    # P  — first character (positive indexing)
print(word[-1])   # n  — last character (negative indexing)
print(word[2])    # t  — third character
print(word[-2])   # o  — second-to-last character
Walking through an example
word = "Python"

print(word[0])   # P
print(word[1])   # y
print(word[5])   # n
print(word[-6])  # P — same character as word[0], counted from the other direction
Common error: IndexError

Try to access a position that doesn't exist, and indexing raises an error rather than silently returning something like an empty result:

word = "Python"
print(word[10])
# IndexError: string index out of range

This is worth internalizing early, because it behaves very differently from slicing, which handles out-of-range positions gracefully — covered next.

Slicing: Extracting Substrings

Basic syntax

Slicing uses the syntax s[start:end], where start is inclusive and end is exclusive — the character at the end position itself is not included in the result.

word = "Python"

print(word[0:3])   # Pyt — characters at index 0, 1, 2 (not 3)
print(word[2:5])   # tho — characters at index 2, 3, 4 (not 5)
Omitting start or end

Leaving either side blank tells Python to default to the beginning or end of the string:

word = "Python"

print(word[:3])    # Pyt — from the start up to (not including) index 3
print(word[3:])    # hon — from index 3 to the end
print(word[:])     # Python — the entire string, unchanged
Negative indices in slices

You can mix negative indices into slicing the same way you can with plain indexing:

word = "Python"

print(word[-3:])    # hon — the last 3 characters
print(word[:-3])    # Pyt — everything except the last 3 characters
print(word[-4:-1])  # tho — from the 4th-from-last up to (not including) the last
Slicing never raises an error

This is the key difference from indexing, and it's worth remembering: slicing handles out-of-bounds positions gracefully, simply returning as much as it can, rather than raising a python IndexError.

word = "Python"

print(word[2:100])   # thon — Python just stops at the actual end of the string
print(word[100:200]) # '' — an empty string, no error at all

If you try the equivalent with plain indexing — word[100] — you'd get an IndexError immediately. Slicing is deliberately more forgiving, which makes it safer to use when you're not certain a given range actually exists within the string.

The Step Parameter: s[start:end:step]

Slicing supports a third component: step, which controls how many characters to skip between each one included in the result.

Skipping characters
word = "Python"

print(word[::2])    # Pto — every second character, starting from index 0
print(word[1::2])   # yhn — every second character, starting from index 1
print(word[::3])    # Ph  — every third character
Negative step: reversing direction

A negative step tells Python to walk backward through the string instead of forward:

word = "Python"

print(word[::-1])    # nohtyP — the entire string, reversed
print(word[5:0:-1])  # nohty — from index 5 down to (not including) index 0

That first example — s[::-1] — is the standard idiom for reverse string python: no start or end specified (so it defaults to covering the whole string), and a step of -1 walking backward one character at a time.

Practical example: extracting a pattern

The step parameter is genuinely useful any time you need to pull out data at a regular interval — extracting every third character from a fixed-format code, or reading a simple pattern embedded at regular positions within a string:

encoded = "HXeXlXlXoX"
message = encoded[::2]
print(message)   # Hello — every other character reconstructs the hidden message

Practical Applications and Immutability

Parsing fixed-width data

Slicing is a natural fit whenever you're working with data that has a predictable, fixed structure — a date string in a known format, or a log line where certain fields always sit at the same position:

date_string = "20260709"   # YYYYMMDD

year = date_string[:4]
m date_string[4:6]
day = date_string[6:]

print(f"{year}-{month}-{day}")   # 2026-07-09
Inserting or replacing substrings via slicing and concatenation

Because strings are immutable, you can't directly insert or replace a portion of a string in place — but slicing combined with concatenation gets you there by building a new string:

word = "Python"

# "Insert" a substring by slicing around the target position
modified = word[:2] + "XYZ" + word[2:]
print(modified)   # PyXYZthon
Reminder: strings are immutable

As covered in the earlier strings article, this is worth restating here specifically: neither indexing nor slicing ever modifies the original string. Both always return something new — a single character, or a new substring — leaving the original completely untouched.

word = "Python"
sliced = word[0:3]

print(word)     # Python — unchanged
print(sliced)   # Pyt — a separate new string
Quick example: checking for palindromes

A neat, compact use of the reversing idiom from earlier: checking whether a string reads the same forward and backward.

def is_palindrome(s):
    return s == s[::-1]

print(is_palindrome("racecar"))   # True
print(is_palindrome("python"))    # False

This works because s[::-1] produces the string reversed, and comparing it directly against the original with == tells you immediately whether the two match — no loop, no manual character-by-character comparison required.