Module 2 · Core Data Structures ⏱ 19 min

Strings & f-strings

By the end of this lesson you will be able to:
  • Index and slice a string, including negative indices, omitted bounds, and step
  • Call common string methods and explain why strings are immutable
  • Compose values into text with f-strings and basic format specifiers

Most programs spend more time handling text than doing math. User names, addresses, log files, web pages, configuration files — everything arrives as strings. Python gives you powerful tools to inspect, slice, clean, and format that text. But strings have a hidden rule that trips up every beginner: they cannot be changed once created. You can build new strings from old ones, but you can never edit a character in place. Understanding that rule — and the library of methods that work around it — is the difference between code that looks right and code that actually works.

In this lesson you will learn to pull out substrings with indexing and slicing, transform text with methods, and compose values into readable messages with f-strings. These are the skills you will use in every Python program you write.

flowchart LR
  A["word[0]"] --> B["'P'"]
  C["word[-1]"] --> D["'n'"]
  E["word[0:3]"] --> F["'Pyt'"]
Indexing and slicing a string — the end of a slice is exclusive.

Every character in a string sits at an index, starting from zero. word[0] is the first character, word[1] the second, and so on. But the real power is slicing: word[start:stop] grabs a range of characters, from start up to — but not includingstop. That off-by-one behavior is deliberate and consistent across Python, but it takes practice to stop guessing. If you omit start, the slice begins at the beginning; omit stop, and it runs to the end. Both can be negative, so word[:-1] means everything except the last character.

There is a third number you can add: step. word[::2] reads every second character, starting from the beginning and running to the end. word[::-1] is the famous reversal trick — a step of -1 walks backwards. The full slice syntax is word[start:stop:step], and any of the three numbers can be left out.

Slicing with positive indices, negative indices, and step.
word = "Python"

print(word[0])       # first character
print(word[-1])      # last character
print(word[0:3])     # first three (stop is exclusive)
print(word[2:])      # from index 2 to the end
print(word[:2])      # first two characters
print(word[::2])     # every second character
print(word[::-1])    # reversed

Negative indices count from the right, which is far more useful than it first appears. word[-1] is the last character, word[-2] the second-to-last. This means you never need to know the length of a string to touch its tail. A common pattern is word[:n] for the first n characters and word[-n:] for the last n. The colon is what tells Python you want a slice, not a single character — word[2] is one character, but word[2:] is a substring starting there.

One subtlety: slicing past the end of a string does not raise an error. word[2:999] simply stops at the last character. This is safe and forgiving, which is why slicing is the preferred way to crop text when you are not sure of the exact length.

flowchart LR
  S["'Python'"] --> F["[::-1]"]
  F --> R["'nohtyP'"]
  S --> E["[::2]"]
  E --> O["'Pto'"]
  style F fill:#3776ab,color:#fff
  style E fill:#1e293b,color:#fff
A step of -1 walks backwards; omitted bounds reach the edge automatically.

Strings come with dozens of built-in methods.upper(), .lower(), .strip(), .replace(), .find(), .startswith() — that return new strings rather than editing the original. This is the immutability rule in action. When you write name.upper(), Python builds a brand-new string with capital letters and hands it back; name itself is untouched. If you want to keep the result, you must assign it: name = name.upper(). Forgetting this is the source of a classic silent bug: the method runs, the result is discarded, and the original string looks unchanged.

Two methods worth distinguishing: .find() returns -1 when a substring is missing, while .index() raises a ValueError. Both return the position of the first match when the substring exists, but .find() is safer when you are not sure the text is there.

Methods return new strings; the original stays the same.
text = "  hello world  "
clean = text.strip()

print(clean)
print(clean.upper())
print(clean.replace("world", "Python"))
print(clean.startswith("hello"))
print(clean.find("Python"))  # -1 because not found
print("original is still:", text)

Building text by gluing strings together with + gets awkward fast, especially when numbers are involved. An f-string lets you drop values straight into the text. Prefix the quotes with f, then wrap any expression in curly braces: f"Hello, {name}". Python evaluates the expression, converts it to text, and slots it in. You can even format numbers: f"{price:.2f}" rounds a float to two decimal places. This is cleaner, faster, and avoids the TypeError that + throws when you accidentally mix strings and numbers.

Because f-strings are expressions, you can call functions inside the braces: f"You have {len(items)} items". The result is readable, self-contained, and avoids the temporary variables that concatenation often forces you to create.

flowchart LR
  F["f'Price: {price:.2f}'"] --> E["evaluate price:.2f"]
  E --> R["'Price: 4.50'"]
  style F fill:#3776ab,color:#fff
  style E fill:#1e293b,color:#fff
An f-string evaluates expressions inside braces and converts them to text.
Embedding values and formatting numbers with f-strings.
item = "coffee"
price = 4.5
qty = 3
total = price * qty

print(f"Item: {item}")
print(f"Unit price: ${price:.2f}")
print(f"Qty: {qty}")
print(f"Total: ${total:.2f}")
print(f"Length: {len(item)} letters")

The braces in an f-string are not just placeholders — they are full expressions. f"{len(word)} letters" evaluates len(word) right there. For numbers, a colon inside the braces triggers formatting: :.2f for two decimals, :>10 for right-alignment in ten characters. This replaces older approaches like "%s" formatting and "{}.format()", both of which you will still see in older code but should avoid in new code. Reach for f-strings first; they are readable, fast, and the modern standard.

One caution: the expression inside braces is evaluated immediately. If it is expensive or has side effects, consider computing it beforehand. For simple values and formatting, though, f-strings are almost always the right choice.

Exercise

Write shout(text) that returns text uppercased with a "!" on the end. (Example: shout("hi")"HI!".)

def shout(text):
    # uppercase text and add "!"
    pass
Exercise

What does this print? Remember slices are exclusive at the end and negative indices count from the right.

word = "Python"
print(word[0])
print(word[-1])
print(word[0:3])
print(word[2:])
print(word[::-1])
Exercise

Write initials(first, last) that returns the initials of a name, separated by a dot. For example, initials("Ada", "Lovelace") should return "A.L.".

def initials(first, last):
    # return initials like "A.L."
    pass
Exercise

This function is meant to return a trimmed, uppercased version of the text, but it leaves the original untouched. Fix it so the operations are actually applied.

def make_title(text):
    text.strip()
    text.upper()
    return text
Exercise

Write mask_card(card_number) that takes a 16-digit string and returns it masked like "****-****-****-1234", showing only the last four digits. Use slicing to extract the tail.

def mask_card(card_number):
    # return masked card number
    pass

Recap

  • Strings are sequences of characters indexed from 0; negative indices count from the end.
  • A slice [start:stop] is exclusive at the end; omit either bound to reach the edge.
  • A third number gives the step: word[::2] for every second character, word[::-1] to reverse.
  • Strings are immutable — methods return new strings, never edit the old one. Save the result with assignment.
  • Use f-strings to embed values in text: f"Value: {x}".
  • Format numbers inside f-strings with :.2f, :>10, and similar specifiers.

Next you will learn how to make your programs respond to the user by reading input.

Checkpoint quiz

What does 'python'.upper() return?

Given s = 'Python', what is s[-1]?

Go deeper — technical resources