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 -1Both 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 characterWalking 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 directionCommon 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 rangeThis 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, unchangedNegative 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 lastSlicing 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 allIf 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 characterNegative 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 0That 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 messagePractical 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-09Inserting 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) # PyXYZthonReminder: 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 stringQuick 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")) # FalseThis 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.