Topic 3.1
Indexing and Slicing
In one line
Every character in a string has a position number, starting at 0, and you can also count from the end with negative numbers. A slice s[start:stop:step] cuts out a piece of the string, never fails for out-of-range numbers, and gives you a new string.
Think of it like this
A row of numbered lockers. The first locker is number 0, the next is 1, and so on. If you want the last locker but do not know how many there are, you can say "first from the end" instead. A string is a row of lockers, each holding exactly one character, and the locker number is called the index.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Index
- The position number of one character in a string. The first character has index 0.
- Negative index
- A position counted from the end: -1 is the last character, -2 the one before it.
- Slice
- A piece of a string cut out with
[start:stop]or[start:stop:step]. - Start, stop, step
- Where a slice begins (included), where it ends (excluded), and how far it jumps each time.
- IndexError
- The error Python raises when a single index points outside the string.
- Empty string
- A string with no characters, written
"". Its length is 0. - Sequence
- Any ordered row of items you can index and slice. A string is a sequence of characters; lists (Phase 4) are sequences too.
- Out of range
- A position that does not exist in the string, such as index 6 in a six-letter word.
Step by step
01Every character has a place number
Python gives each character a number, starting from 0. You ask for a character by writing the string, then square brackets with the number inside. The result is a one-character string. Python has no separate "character" type: a single letter is just a string of length 1.
The same characters also have negative numbers, counting from the right. Both numberings point at the same letters.
word = "Python"
print(word[0]) # P
print(word[1]) # y
print(word[-1]) # n (the last one)
print(len(word)) # 602Why the last index is length minus one
A string of length 6 has indexes 0, 1, 2, 3, 4, 5. There is no index 6. The last valid index is always len(word) - 1, and word[-1] is the shortcut for it.
Asking for an index that does not exist stops the program with an IndexError. This is one of the most common beginner errors; the cure is to remember that counting starts at 0.
03Slices: a piece between two fence posts
A slice has two numbers separated by a colon. The first is where the piece starts (included). The second is where it stops (excluded). Think of the numbers as fence posts standing between the letters: a|b|c|d|e|f. The slice [2:5] is the stretch of fence from post 2 to post 5, which holds the letters c, d, e.
Excluding the stop is not a quirk; it makes the arithmetic clean. s[2:5] has 5 - 2 = 3 characters. And s[:3] followed by s[3:] rebuilds the whole string with no gap and no overlap.
s = "abcdefghij"
print(s[2:5]) # cde
print(s[:3] + s[3:]) # abcdefghij (nothing lost, nothing repeated)04Leaving numbers out
Each of the two numbers has a default. No start means "the very beginning"; no stop means "the very end". The three most useful slices are all built from this: the first few characters s[:n], the last few s[-n:], and everything but the first few s[n:].
s[:-1] drops the last character. s[-3:] takes the last three. Be careful with s[-0:]: -0 is just 0, so it means "from the start" and returns the whole string, not nothing.
s = "abcdefghij"
print(s[:3]) # abc first three
print(s[7:]) # hij from index 7 to the end
print(s[-3:]) # hij last three
print(s[:-1]) # abcdefghi all but the last05Steps, and reversing a string
The third number is the step: how far to jump after each character. s[::2] takes every second character, starting at the first. s[1::3] starts at index 1 and takes every third.
A negative step walks from right to left. s[::-1] is the idiom for reversing: start at the end, go backwards one at a time, stop at the beginning. With a negative step the default start is the last character and the default stop is "before the first". If you give explicit numbers, they must be in walking order: s[8:2:-2] starts at index 8 and moves down.
s = "abcdefghij"
print(s[::2]) # acegi
print(s[1::3]) # beh
print(s[::-1]) # jihgfedcba
print(s[8:2:-2]) # ige06Slices never raise IndexError
A single index outside the string is an error, because there is nothing to give back. A slice is different: it asks for "whatever lies between these two posts", and if the posts are outside the string, whatever lies there is simply less (or nothing). Python clamps the numbers to the string's real ends.
This is a deliberate design choice. It lets you write text[:80] to mean "at most the first 80 characters" without first checking the length. A short text just comes back whole.
repr(...) shows a string with its quotes, so an empty string appears as '' instead of as nothing.
s = "abc"
print(s[1:100]) # bc
print(repr(s[50:60])) # '' (an empty string, not an error)
print(s[:80]) # abc (safe even for short text)07A slice is a new value
A slice never changes the original string; it creates a new string holding the chosen characters. Strings cannot be changed at all (the reason is in Topic 3.5), so there is nothing to damage.
Behind the scenes, CPython (the standard Python) may hand back the very same value in memory for s[:], because sharing an unchangeable value is safe and cheaper. You cannot tell the difference from your code. It starts to matter with lists in Topic 4.2, where items[:] really does copy.
Python also lets you keep a slice in a variable with slice(start, stop, step) and use it later as s[my_slice]. You will see this inside libraries more than in everyday code.
Try it yourself
- 1
Predict, then run
In the second example, write down on paper what you think
s[1:4],s[-4:-1]ands[::-2]give. Then add threeprintlines and run them. Wrong guesses are the fastest way to learn the stop-is-excluded rule. - 2
Make a username
Take
name = "Hectal Learner". Print the first three letters in capitals (name[:3].upper();.upper()makes capital letters, and Topic 3.2 teaches more tools like it), then the last four letters. Try a name shorter than three letters and see that nothing breaks. - 3
Find the off-by-one
Change
date[5:7]in the third example todate[5:6]. The month becomes1. Fix it back, then explain in one sentence why[5:7]holds two characters: because7 - 5 = 2.
Code & diagrams
Predict each line before you run it. Then change the word and run again.
Expected output
word: Python
first letter: P
second letter: y
last letter: n
second from the end: o
length: 6
last, counted from the front: nEvery slice here is a new string. The original `s` is never changed.
Expected output
s[2:5] -> cde
s[:3] -> abc
s[7:] -> hij
s[-3:] -> hij
s[::2] -> acegi
s[1::3] -> beh
s[::-1] -> jihgfedcba
s[8:2:-2] -> ige
s[3:100] -> defghij
s[50:60] -> ''
copy equals s: TrueFixed-format text (dates, codes, file names) is where slices shine.
Expected output
year: 2026 month: 10 day: 09
masked card: ************1111
extension: pdf
name without extension: report_final
palindrome: TrueBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Ask for a character that is not there
Run word = "Python" followed by print(word[6]).
word = "Python"
print(word[6])Myth vs fact
Myth
The first character is at index 1.
Fact
It is at index 0. Counting from zero is a convention shared by almost every programming language, and the index measures the distance from the start.
Myth
s[2:5] includes the character at index 5.
Fact
The stop is excluded. s[2:5] holds indexes 2, 3 and 4. This makes the length stop - start and lets s[:k] + s[k:] rebuild the string exactly.
Myth
A slice can crash with IndexError like an index can.
Fact
A slice with numbers outside the string is clamped. Only a single index outside the string raises IndexError.
Myth
Slicing changes the original string.
Fact
Strings cannot change. A slice builds a new string and leaves the original untouched.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Slicing is
s.__getitem__(slice(start, stop, step)). Thesliceobject is a real value, andslice.indices(len(s))returns the clamped(start, stop, step)that Python will actually use. That is exactly how the "forgiving" behaviour is implemented, and it is the same protocol that lists, tuples, bytes and NumPy arrays use. In Topic 8.7 you will make your own class sliceable. - ▸
A slice copies the characters it selects, so
text[1:]on a 100 MB string copies about 100 MB. Taking many slices off the front of a huge string in a loop is quadratic. When you parse big text, move an index forward and slice small pieces, or usestr.partition,str.findwith a start position, ormemoryviewfor bytes. - ▸
A string index returns a
str, never a separate character type, so"abc"[0][0][0]is still"a". Indexes are counted in code points, not in what a person sees as a letter, so a slice can cut an emoji or an accented letter in half (see Topic 3.4).
Remember this
- 1
Indexes start at 0, not 1. In
"Python",word[0]is"P"andword[5]is"n". The reason is that an index measures how far you are from the start: the first character is zero steps away. A string of lengthnhas valid indexes0ton - 1. Asking forword[6]on a six-letter word raises anIndexError(Python's "that position does not exist" error), because there is no seventh character. (len(word)gives the number of characters, here 6.) - 2
Negative indexes count from the end:
word[-1]is the last character,word[-2]the one before it. This is shorthand forword[len(word) - 1], and it is how Python programmers normally ask for the last item.word[-6]is the first character of a six-letter word;word[-7]raisesIndexError. - 3
A slice cuts out a piece:
word[start:stop]gives the characters fromstartup to, but not including,stop. So"abcdefghij"[2:5]is"cde". Becausestopis excluded,word[a:b]has exactlyb - acharacters, andword[:k] + word[k:]is always the whole string again. Picture the numbers as sitting between the characters (on the fence posts), and the slice as the stretch of fence between two posts. - 4
You can leave out either number. A missing
startmeans "from the beginning", a missingstopmeans "to the end", soword[:3]is the first three characters,word[3:]is everything from index 3,word[-3:]is the last three, andword[:]is the whole string. Negative numbers work inside slices too:word[:-1]is everything except the last character. - 5
A slice can have a third number, the step:
word[start:stop:step].word[::2]takes every second character. A negative step walks backwards, soword[::-1]is the string reversed. A step of0is an error. - 6
With a negative step the defaults flip: the start is the end of the string and the stop is the beginning. So
s[8:2:-2]means "start at 8, go down by 2, stop before reaching 2". - 7
Slices are forgiving: where a single index outside the string raises
IndexError, a slice with numbers outside the string is quietly clamped (trimmed to fit the string)."abc"[1:100]is"bc"and"abc"[50:60]is""(the empty string). A slice always gives you a new string. Strings never change (Topic 3.5), so slicing never damages the original; it builds a new value from part of it.
Explain it without notes
Why do Python indexes start at 0, and what is the last valid index of a string?
What does s[a:b] return, and why is b excluded?
Explain how s[::-1] reverses a string, and what changes about the defaults when the step is negative.
What is the difference between s[10] and s[10:11] when s is shorter than 10 characters?
Practice
Given word = "Hectal", print its first three letters, its last three letters and the word reversed.
Given s = "0123456789", print every third character starting from index 1.
Given email = "user@example.com", find the position of @ with email.find("@") (find gives the index of the first match; Topic 3.2 teaches it) and print the part after it and the part before it.
Trade-offs
- ↔
Slices are concise but cryptic.
s[::-1]is idiomatic, yet a reader who has not seen it needs a moment. For anything beyond the common patterns, give the pieces names (year = date[:4]) so the code explains itself. - ↔
Forgiving slices hide bugs.
text[:80]is convenient because it never fails, butdate[5:7]on a too-short string silently returns a short or empty result. When the format must be exact, checklenfirst or validate with a regular expression (Topic 3.6).
Done when you can
Done when you can say the index of the first and last character of any string, in both directions.
Done when you can predict
s[a:b]for any a and b, including negative ones, and explain why b is excluded.Done when you can write the slices for first n, last n, all but the last, every second character and reversed.
Done when you can explain why
s[100]raises an error buts[100:200]does not.Done when you can explain why a slice does not change the original string.