Python String Slicing
String slicing is how you pull a piece — or a rearranged copy — out of a Python string without writing a single loop. Instead of manually walking through characters with indices, you describe a range with a compact [start:stop:step] syntax, and Python hands you back a brand-new string containing exactly what you asked for. Slicing is one of the most-used tools in everyday Python because it works identically on strings, lists, and tuples, and because it never raises an error even when your range is “wrong” — it just clamps to what’s available.
Overview / How It Works
A Python string is a sequence of characters, and every sequence supports two related but different operations: indexing and slicing. Indexing with s[i] asks for a single character at position i and raises IndexError if that position doesn’t exist. Slicing with s[start:stop] asks for a range of positions and always succeeds, returning an empty string in the worst case rather than crashing.
Internally, when you write s[start:stop:step], Python builds a slice object — the same object you’d get by calling slice(start, stop, step) directly — and passes it to the string’s __getitem__ method. The string then calls slice.indices(len(s)), which normalizes your (possibly negative, possibly out-of-range, possibly omitted) numbers into a safe, in-bounds triple of actual integers. That normalized triple is what actually gets used to walk the underlying character data and build the new string. This is why slicing is so forgiving: the clamping happens before any character is ever touched.
Because Python strings are immutable, slicing never modifies the original string — it always constructs a new string object. The original is completely untouched, no matter how the slice is used later.
Indices in a string of length n run from 0 to n - 1 counting forward, and from -1 to -n counting backward from the end. Index -1 is always the last character, -2 the second-to-last, and so on. Slicing lets you freely mix positive and negative indices in the same expression.
Syntax
sequence[start:stop:step]
All three parts are optional, and each has a sensible default when omitted:
| Part | Meaning | Default if omitted |
|---|---|---|
start |
Index of the first character to include | 0 (or len(s) - 1 if step is negative) |
stop |
Index to stop before (never included) | len(s) (or -len(s) - 1, effectively “before the start”, if step is negative) |
step |
How many positions to advance each time; negative reverses direction | 1 |
The single most important rule to memorize: stop is exclusive. s[2:5] gives you characters at indices 2, 3, and 4 — three characters, not four.
Examples
Example 1: Basic slicing with start and stop
message = "Hello, Python World!"
# Basic slicing: [start:stop]
greeting = message[0:5]
print(greeting)
subject = message[7:13]
print(subject)
# Omitting start or stop
first_word = message[:5]
print(first_word)
rest = message[7:]
print(rest)
# Full copy
copy_of_message = message[:]
print(copy_of_message)
Output:
Hello
Python
Hello
Python World!
Hello, Python World!
Notice that message[0:5] and message[:5] produce the same result — when start is 0 you can simply leave it out. Also notice message[:]: an empty slice copies the entire string. This is a common idiom for making an explicit copy of a sequence (though for immutable strings there’s rarely a practical need, since the original can’t change anyway).
Example 2: Negative indices and the step value
text = "Programming"
print(text[-1])
print(text[-4:])
print(text[:-4])
print(text[::-1])
print(text[::2])
print(text[1:-1:2])
Output:
g
ming
Program
gnimmargorP
Pormig
rgamn
text[-1] grabs the last character. text[-4:] means “start 4 characters from the end, go to the end,” giving "ming". text[:-4] means the opposite: everything up to (but not including) the last four characters. text[::-1] is the famous “reverse a string” idiom — an empty start and stop with step = -1 walks the whole string backward. text[::2] takes every second character starting from the beginning, and text[1:-1:2] combines all three parts: start at index 1, stop before the last character, and step by 2.
Example 3: A realistic use — truncating text and reading file extensions
def truncate(text: str, max_length: int) -> str:
if len(text) <= max_length:
return text
return text[:max_length - 3] + "..."
def get_file_extension(filename: str) -> str:
dot_index = filename.rfind(".")
if dot_index == -1:
return ""
return filename[dot_index + 1:]
headline = "Python slicing lets you extract substrings without loops"
short_headline = truncate(headline, 30)
print(short_headline)
filenames = ["report.pdf", "archive.tar.gz", "README"]
for name in filenames:
ext = get_file_extension(name)
print(f"{name} -> {ext!r}")
Output:
Python slicing lets you ext...
report.pdf -> 'pdf'
archive.tar.gz -> 'gz'
README -> ''
truncate uses text[:max_length - 3] to leave exactly enough room for the trailing "...". get_file_extension combines str.rfind (which locates the last dot, important for names like archive.tar.gz) with a slice from just after that dot to the end. When there’s no dot at all, rfind returns -1 and the function returns an empty string instead of slicing incorrectly.
Under the Hood: How Python Resolves a Slice
When Python evaluates s[start:stop:step], it goes through roughly these steps:
- The three values (or their defaults) are packaged into a
sliceobject:slice(start, stop, step). - The string’s length,
len(s), is used to callslice.indices(len(s)), which returns a normalized(start, stop, step)triple where every negative index has been converted to its positive equivalent and every out-of-bounds value has been clamped into the valid range[0, len(s)]. - Python walks the string from the normalized
startto the normalizedstop, advancing bystepeach time, collecting each character it lands on. - A new string object is allocated and returned containing exactly those characters, in order.
You can see this normalization directly:
s = "Slicing"
print(slice(2, 100).indices(len(s)))
This prints (2, 7, 1) — Python has already clamped the impossible stop=100 down to the string’s actual length, 7. That clamping is precisely why slicing never raises IndexError even when your numbers overshoot: by the time any character lookup happens, the range has already been made safe.
Common Mistakes
Mistake 1: Forgetting that stop is exclusive
It’s easy to assume a slice includes the character at the stop index. This leads to off-by-one bugs, especially when extracting a fixed-width field from a formatted string:
filename = "report_2024.csv"
wrong_year = filename[8:11]
print(wrong_year)
Output:
024
The digits "2024" actually start at index 7, not 8, so this slice cuts off the leading "2" and grabs one character too many from the wrong end. Counting indices carefully (or better, locating the boundary programmatically instead of hardcoding numbers) fixes it:
filename = "report_2024.csv"
year = filename[7:11]
print(year)
Output:
2024
Mistake 2: Expecting a slice to raise IndexError like direct indexing does
Because s[i] raises IndexError for an out-of-range i, many beginners assume s[start:stop] will too. It won’t — slicing silently clamps to whatever exists, which can hide bugs where you expected a crash to alert you to bad input:
data = "abc"
print(data[10:20])
print(data[1:100])
print(data[-100:2])
Output:
bc
ab
data[10:20] is entirely past the end of a 3-character string, so it quietly returns an empty string instead of erroring. data[1:100] clamps its huge stop down to the actual length. data[-100:2] clamps its huge negative start up to 0. If you actually need to know whether an index is valid — for example while parsing untrusted input — check len(s) explicitly rather than relying on slicing to catch the problem for you, since data[10] (plain indexing, no colon) is the version that would raise IndexError.
Best Practices
- Remember
stopis exclusive — reads[a:b]as “b - acharacters starting ata” to avoid off-by-one mistakes. - Prefer
s[:n]ands[n:]overs[0:n]ands[n:len(s)]— the shorter forms are idiomatic and communicate “from the start” / “to the end” clearly. - Use negative indices (
s[-n:]) instead of computinglen(s) - nmanually when you want the lastncharacters. - For readability, prefer built-in string methods over manual slicing when one exists: use
s.startswith(prefix)instead ofs[:len(prefix)] == prefix, ands.removeprefix(p)/s.removesuffix(p)(Python 3.9+) instead of hand-rolled slices for trimming known prefixes or suffixes. - If a slice’s start/stop values are meaningful (not just “first 5 characters”), give them names — e.g.
header, body = line[:HEADER_LEN], line[HEADER_LEN:]— rather than leaving magic numbers in the code. - Use
s[::-1]for reversing short strings; for very large strings or when clarity matters more than brevity, consider"".join(reversed(s))instead. - Don’t rely on slicing to validate input ranges — it clamps silently instead of erroring, so add explicit bounds checks where correctness matters.
Practice Exercises
- Write a function
middle_initial(full_name: str) -> strthat takes a string like"Ada Marie Lovelace"and returns just the middle name’s first letter followed by a period, e.g."M.", using slicing (notsplit) on the already-located middle name. - Given
phone = "+1-415-555-0182", use slicing to extract the area code"415"and the last four digits"0182"into two separate variables, then print them as"Area: 415, Last4: 0182". - Write a function
is_palindrome(word: str) -> boolthat uses a single slicing expression to check whether a string reads the same forwards and backwards (e.g."level"is a palindrome,"world"is not).
Summary
- Slicing uses
s[start:stop:step]to extract a substring; all three parts are optional and have defaults (0,len(s), and1respectively). stopis always exclusive — the character at that index is never included.- Negative indices count from the end of the string, with
-1being the last character. - A negative
stepwalks backward, which is hows[::-1]reverses a string. - Slicing is forgiving: out-of-range
start/stopvalues are clamped rather than raising an error, unlike direct indexing withs[i]. - Slicing always returns a brand-new string, since strings are immutable — the original is never modified.
- Prefer built-in methods (
startswith,removeprefix, etc.) over manual slicing when they express the same intent more clearly.
