Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~27 min· topic 1 of 7

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.

main.pywhole filepython
word = "Python"

print(word[0])     # P
print(word[1])     # y
print(word[-1])    # n  (the last one)
print(len(word))  # 6
Every character has a place numberdiagram
Rendering diagram…

02Why 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.

terminal
$ python main.py
── expected output ──
Traceback (most recent call last):
File "main.py", line 2, in <module>
print(word[6])
~~~~^^^
IndexError: string index out of range

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.

main.pywhole filepython
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.

main.pywhole filepython
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 last

05Steps, 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.

main.pywhole filepython
s = "abcdefghij"

print(s[::2])       # acegi
print(s[1::3])      # beh
print(s[::-1])      # jihgfedcba
print(s[8:2:-2])    # ige

06Slices 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.

main.pywhole filepython
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. 1

    Predict, then run

    In the second example, write down on paper what you think s[1:4], s[-4:-1] and s[::-2] give. Then add three print lines and run them. Wrong guesses are the fastest way to learn the stop-is-excluded rule.

  2. 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. 3

    Find the off-by-one

    Change date[5:7] in the third example to date[5:6]. The month becomes 1. Fix it back, then explain in one sentence why [5:7] holds two characters: because 7 - 5 = 2.

Code & diagrams

Indexes count from zero New tab

Predict each line before you run it. Then change the word and run again.

Sign in to run this example in your browser.

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: n
Slices of every shape New tab

Every slice here is a new string. The original `s` is never changed.

Sign in to run this example in your browser.

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: True
Slicing real data New tab

Fixed-format text (dates, codes, file names) is where slices shine.

Sign in to run this example in your browser.

Expected output

year: 2026 month: 10 day: 09
masked card: ************1111
extension: pdf
name without extension: report_final
palindrome: True
Index numbers on a stringdiagram
Rendering diagram…

Break 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]).

main.pywhole filepython
word = "Python"
print(word[6])
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 2, in <module>
print(word[6])
~~~~^^^
IndexError: string index out of range

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)). The slice object is a real value, and slice.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 use str.partition, str.find with a start position, or memoryview for 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. 1

    Indexes start at 0, not 1. In "Python", word[0] is "P" and word[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 length n has valid indexes 0 to n - 1. Asking for word[6] on a six-letter word raises an IndexError (Python's "that position does not exist" error), because there is no seventh character. (len(word) gives the number of characters, here 6.)

  2. 2

    Negative indexes count from the end: word[-1] is the last character, word[-2] the one before it. This is shorthand for word[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] raises IndexError.

  3. 3

    A slice cuts out a piece: word[start:stop] gives the characters from start up to, but not including, stop. So "abcdefghij"[2:5] is "cde". Because stop is excluded, word[a:b] has exactly b - a characters, and word[: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. 4

    You can leave out either number. A missing start means "from the beginning", a missing stop means "to the end", so word[:3] is the first three characters, word[3:] is everything from index 3, word[-3:] is the last three, and word[:] is the whole string. Negative numbers work inside slices too: word[:-1] is everything except the last character.

  5. 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, so word[::-1] is the string reversed. A step of 0 is an error.

  6. 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. 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

01

Why do Python indexes start at 0, and what is the last valid index of a string?

02

What does s[a:b] return, and why is b excluded?

03

Explain how s[::-1] reverses a string, and what changes about the defaults when the step is negative.

04

What is the difference between s[10] and s[10:11] when s is shorter than 10 characters?

Practice

01

Given word = "Hectal", print its first three letters, its last three letters and the word reversed.

02

Given s = "0123456789", print every third character starting from index 1.

03

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, but date[5:7] on a too-short string silently returns a short or empty result. When the format must be exact, check len first 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 but s[100:200] does not.

  • Done when you can explain why a slice does not change the original string.