Chapter 06

Working with text — indexing, slicing, and f-strings

Pulling pieces out of text, cleaning it, changing it, and building readable messages with f-strings — plus the rule that text cannot be modified in place.

35 minPython 3.12
  1. 1Encounter
  2. 2Understand
  3. 3Worked
  4. 4Predict
  5. 5Apply
  6. 6Stretch

The problem we are solving

A name arrives from a form: " Rafi Ahmed ". Extra spaces at both ends, and what you need is the first name only, plus a message — "Welcome, Rafi!".

That work — cleaning text, pulling pieces out of it, and building something new from them — is among the most frequent tasks in programming. Usernames, email addresses, ids, filenames, addresses, whatever an API sends back: all text.

And one more thing will become clear today, the kind that costs hours when you do not know it: in Python, text cannot be modified. Writing name.upper() does not make name uppercase. Why not, and what to do instead, is the most valuable part of this mission.

By the end of this mission you can

  • Reach any character by index, from the front and from the back
  • Cut a section out with a slice
  • Clean and change text with strip, upper, lower, replace and split
  • Explain why the result of a method has to be kept
  • Build readable messages with f-strings

Prerequisites: Variables and types.


Quotes — one or two

To Python, 'Rafi' and "Rafi" are the same thing. Both exist so that quotes inside the text cause no trouble:

python
print("It's working")
print('She said "yes"')
text
It's working
She said "yes"

Three quotes hold text that spans several lines:

python
note = """Dear customer,
Your order is confirmed.
Thank you."""
print(note)
text
Dear customer,
Your order is confirmed.
Thank you.

And \n produces a new line, \t a tab:

python
print("Name:\tRafi\nRole:\tLearner")
text
Name:	Rafi
Role:	Learner

Length and index

python
word = "PYTHON"

print(len(word))
print(word[0])
print(word[1])
print(word[-1])
print(word[-2])
text
6
P
Y
N
O

Two things to fix in your head:

Counting starts at zero. The first character is index 0, the second 1. So in a six-character word the last index is 5, not 6. Ask for word[6] and you get an IndexError. This "one too many" mistake is so common it has its own name: an off-by-one error.

Negative indexes count from the back. word[-1] is the last character, word[-2] the one before. It is the easy way to reach the end without knowing the length.

Slices — cutting a section out

python
word = "PROGRAMMING"

print(word[0:7])
print(word[7:11])
print(word[:7])
print(word[7:])
print(word[-4:])
text
PROGRAM
MING
PROGRAM
MING
MING

word[a:b] means from a up to but not including b. The end index is left out.

That feels awkward at first, and it buys you something: word[0:7] and word[7:11] join up exactly, with no character repeated and none missed. Leaving out the first number means "from the very beginning", and leaving out the second means "all the way to the end".

Text cannot be modified

This is the most important rule in the mission:

python
name = "rafi"
name[0] = "R"
text
TypeError: 'str' object does not support item assignment

A character inside existing text cannot be swapped out. So how does uppercasing or trimming work at all? The answer: Python does not change the original, it builds a new piece of text and hands it back.

python
name = "  rafi  "

print(name.strip())
print(name.strip().upper())
print("After all that, name is still:", name)
text
rafi
RAFI
After all that, name is still:   rafi

name did not change at all. strip() produced a new, clean piece of text, and we merely printed it — we never stored it.

So you have to keep the result:

python
name = "  rafi  "
clean_name = name.strip().upper()
print(clean_name)
text
RAFI

Or assign it back to the same name — name = name.strip(). This one mistake, calling a method and dropping the result, happens to everybody early on. And it raises no error, which is why it takes a while to spot.

The methods that earn their keep

python
raw = "  Rafi Ahmed  "

print(raw.strip())
print(raw.strip().lower())
print(raw.strip().upper())
print(raw.strip().replace(" ", "_"))
print(len(raw), len(raw.strip()))
print("rafi" in raw.lower())
text
Rafi Ahmed
rafi ahmed
RAFI AHMED
Rafi_Ahmed
14 10
True
  • strip() — trims whitespace at the start and end (not in the middle)
  • lower() / upper() — changes case
  • replace(a, b) — puts b everywhere a appeared
  • in — says whether one piece of text occurs inside another

And split() breaks text into pieces:

python
full_name = "Rafi Ahmed Khan"
parts = full_name.split(" ")

print(parts)
print(parts[0])
print(len(parts))
text
['Rafi', 'Ahmed', 'Khan']
Rafi
3

The thing in square brackets is a list, which has a mission of its own. For now just know that split() returns the pieces that way, and that an index gets one piece out.

Clean before you compare. If a user types "Rafi ", that is not equal to "rafi" — a trailing space and a capital letter make three differences between them. Which is why running .strip().lower() before comparing is close to a universal habit in real projects.

f-strings — the easy way to build a message

Mixing text and numbers has been clumsy so far. An f-string makes it simple: an f before the quote, and anything you like inside braces:

python
name = "Rafi"
items = 3
price = 12.5

print(f"Hello, {name}!")
print(f"{items} items cost {items * price}")
print(f"Rounded: {items * price:.2f}")
text
Hello, Rafi!
3 items cost 37.5
Rounded: 37.50

Notice three benefits: you can calculate inside the braces (items * price), you never think about types (numbers become text on their own), and :.2f pins the number of decimal places — which every display of a monetary figure needs.

Have a look at what happens when the f is forgotten:

python
name = "Rafi"
print("Hello, {name}")
text
Hello, {name}

No error, just a wrong message. Having seen it once makes it easy to catch later.


A complete example

account.py:

python
# Turn a raw form entry into a clean display name and a login id
raw_input_name = "   Rafi Ahmed   "
domain = "example.com"

clean = raw_input_name.strip()
parts = clean.split(" ")
first_name = parts[0]
last_name = parts[-1]

login_id = f"{first_name.lower()}.{last_name.lower()}"
email = f"{login_id}@{domain}"
initials = f"{first_name[0]}{last_name[0]}"

print(f"Display name : {clean}")
print(f"Login id     : {login_id}")
print(f"Email        : {email}")
print(f"Initials     : {initials}")
print(f"Name length  : {len(clean)} characters")
text
Display name : Rafi Ahmed
Login id     : rafi.ahmed
Email        : rafi.ahmed@example.com
Initials     : RA
Name length  : 10 characters

Why parts[-1]? Because someone's name may have three parts. Taking the last piece from the back works whether there are two parts or three.


When it breaks

IndexError: string index out of range The index you asked for is past the end of the text. Remember that if len(word) is 6, the valid indexes are 0 to 5. For the last character, word[-1] is the safest thing to write.

TypeError: 'str' object does not support item assignment You tried to change one character inside text. That is not possible — build new text instead, e.g. word = "R" + word[1:].

I called the method and nothing changed The result was not stored anywhere. name.strip() does not change name; you need name = name.strip().

strip() is not removing the spaces in the middle strip() only works at the two ends. To remove the inner ones use replace(" ", "").

The f-string message comes out wrong Check for the f immediately before the quote. Without it, braces are printed as ordinary characters.

NameError — the name inside the f-string is unknown Whatever you wrote inside the braces was never created, or is misspelled.