1.2.12 · D4Introduction to Programming (Python)

Exercises — String methods — upper, lower, strip, split, join, replace, find, format

2,852 words13 min readBack to topic
Figure — String methods — upper, lower, strip, split, join, replace, find, format

Level 1 — Recognition

Can you predict what a single method call returns?

Recall Solution L1.1

.upper()"HELLO, WORLD! 42" .lower()"hello, world! 42" Why: casing touches only letters. The comma, space, !, and the digits 42 are left exactly as they were. upper returns one new string, lower returns another — the original is unchanged.

Recall Solution L1.2

Result: "code" — length 4. Why: strip() removes leading and trailing whitespace (spaces, tabs \t, newlines \n). The space between letters would survive, but here there is none inside code. Only the edges are trimmed; the middle is untouched.

Recall Solution L1.3
  • "banana".find("a")1 (first a sits at index 1: b=0, a=1).
  • "banana".find("na")2 (the substring na first starts at index 2).
  • "banana".find("z")-1 (absent → the sentinel, not an error). Why the indices: remember from Strings — indexing and slicing that positions count from 0. find reports the start index of the first match.

Level 2 — Application

Chain two or three methods to reach a goal.

Recall Solution L2.1

line.split(",")['id', 'name', 'city', 'zip']4 elements. Why: split(",") cuts at every comma, producing a list of the pieces between commas. Three commas produce four pieces (the separators sit between items). The result is a list, so it can be indexed and looped — see Lists.

Recall Solution L2.2

"-".join(parts)"a-b-c" — it contains 2 dashes. Why: join places the separator between consecutive items only. For items there are separators, so dashes. This is the exact inverse of split: "-".join("a-b-c".split("-")) == "a-b-c".

Recall Solution L2.3

Result: "bob@gmail.com". Why chain in this order: strip() first snips the leading spaces and the trailing space + \n, returning a new string. Because that return is a string, we can immediately call .lower() on it, which cases every letter down. Emails are case-insensitive, so normalising makes == comparisons reliable — this is the classic pattern from User input and validation.

Recall Solution L2.4
  • s.replace(".", "/")"a/b/c/d" (every . swapped).
  • s.replace(".", "/", 2)"a/b/c.d" (only the first 2 swapped; the count argument caps it). Why: by default replace swaps every occurrence. The optional third argument limits how many replacements happen, counting from the left.

Level 3 — Analysis

Reason about edge cases and why methods behave as they do.

Recall Solution L3.1

It prints "no a" — which is wrong, because "apple" obviously contains a. Why the bug: text.find("a") returns 0 (the a is at index 0). In an if, the number 0 is falsy, so Python jumps to the else. The method did find the letter; the truth test just misreads the index 0 as "nothing here." Correct code: if text.find("a") != -1: or simply if "a" in text:. Both print "has a".

Recall Solution L3.2
  • "".split(",")[''] — one empty piece. Splitting an empty string still yields a list holding a single empty string, not [].
  • ",,".split(",")['', '', ''] — two commas make three pieces, all empty.
  • "a,".split(",")['a', ''] — the trailing comma leaves an empty piece after a. Why: split(sep) always produces (number of separators) + 1 pieces. Empty regions between/around separators become empty strings, never vanish. This is the "edge issue" the parent note warned join could not always undo.
Recall Solution L3.3
  • , , so the last valid start index is . The loop runs i from 0 to 8 inclusive (range(9)).
  • "the cat sat".find("cat")4, because the slice s[4:7] is "cat". Why : a length-3 substring needs 3 characters to fit. Starting any later than index 8 would run off the end of the 11-character string, so 8 is the last place it can still fit.

Level 4 — Synthesis

Assemble several tools into a small program.

Recall Solution L4.1
row = "  Alice , 42 ,  NYC  "
fields = [f.strip() for f in row.split(",")]
# fields -> ['Alice', '42', 'NYC']
clean = ",".join(fields)
# clean  -> 'Alice,42,NYC'

Final string: "Alice,42,NYC". Why this order: split(",") first cuts into [' Alice ', ' 42 ', ' NYC ']. Each piece still carries its own spaces, so we strip() each piece individually inside the list comprehension. Stripping the whole row first would NOT clean the spaces sitting around the interior commas. Finally join glues the cleaned fields with a single comma.

Recall Solution L4.2
sentence = "  the   quick  brown fox  "
words = sentence.split()          # ['the','quick','brown','fox']
count = len(words)                # 4
tidy  = " ".join(words)           # 'the quick brown fox'

Word count = 4; tidy sentence = "the quick brown fox". Why bare split(): it collapses every run of whitespace and drops the empty edge pieces automatically, so the messy multiple spaces become a clean four-word list. Then a single-space join reassembles it — exactly the SLuSH-J flow (Strip is implicit here, Split, Join).

Recall Solution L4.3
name, score = "Bob", 0.8736
msg = "{} scored {:.1%}".format(name, score)
# -> 'Bob scored 87.4%'

Final string: "Bob scored 87.4%". Why {:.1%}: the % presentation type multiplies the value by 100 and appends a percent sign; .1 requests one decimal place. So 0.873687.36 → 87.4% (rounded to one decimal). This separates the raw value from how it is displayed — the same idea drives modern f-strings.


Level 5 — Mastery

Full pipelines, inverse operations, and reasoning about correctness.

Recall Solution L5.1
raw = "  Jean-Luc  PICARD \n"
key = " ".join(raw.strip().lower().split())
# step 1  raw.strip()  -> "Jean-Luc  PICARD"
# step 2  .lower()     -> "jean-luc  picard"
# step 3  .split()     -> ['jean-luc', 'picard']
# step 4  " ".join(..) -> 'jean-luc picard'

Result: "jean-luc picard". Why this exact chain (SLuSH-J): strip trims the outer whitespace and the \n; lower normalises case so "PICARD" and "picard" compare equal; bare split() collapses the internal double space into a clean two-word list; join reassembles with single spaces. Each method returns a new string, so we chain directly. The hyphen inside Jean-Luc is untouched — it is not whitespace, so split() never breaks on it.

Recall Solution L5.2
  • Holds: s = "a,b,c". Then ",".join("a,b,c".split(",")) = ",".join(['a','b','c']) = "a,b,c" — identical. ✅
  • Fails? No — actually split then join with the same separator is ALWAYS a perfect inverse. Even s = ",a," gives split['', 'a', ''] and ",".join([...])",a,". ✅
  • The real failure is the other direction: join then split is NOT always inverse. parts = ['a,b', 'c']; ",".join(parts) = "a,b,c"; splitting that gives ['a','b','c'] ≠ the original ['a,b','c']. The separator appearing inside a piece breaks it. Why: split records exactly where separators were; rejoining puts them back. But if a data value itself contains the separator, join can no longer tell a real value-comma from a structural one — this is precisely why real CSV needs quoting.
Recall Solution L5.3
def my_find(s, t):
    n, m = len(s), len(t)          # n=11, m=4
    for i in range(n - m + 1):     # i = 0..7
        if s[i:i+m] == t:          # compare 4-char window
            return i
    return -1

Trace the windows of length 4:

  • i=0: "miss""issi"
  • i=1: "issi" == "issi"return 1

Returned index = 1. And indeed "mississippi".find("issi")1. Why not the later issi? There is a second "issi" starting at index 4, but find reports the first match and stops, so we never reach it.

Recall Solution L5.4

Failure mode 1 — match at index 0: if a line starts with ERROR, find returns 0, which is falsy, so the check wrongly rejects a real error line. Failure mode 2 — no match: if ERROR is absent, find returns -1, which is truthy (non-zero), so the check wrongly accepts a line with no error! So the buggy version is wrong in both directions. Correct replacement: if "ERROR" in line: — the in operator returns a genuine boolean and has none of the sentinel confusion. (Or explicitly if line.find("ERROR") != -1:.) Why: find was designed to return a position, not a truth value; mixing the two collides with Boolean truthiness rules where 0 is false and -1 is true.


Active recall

Recall Rapid self-quiz (predict before peeking)

Answer, then check against the exercises above.

What does "banana".find("z") return?
-1 (sentinel for absent, not an error).
How many pieces does "a,,b".split(",") give?
3 → ['a', '', 'b'] (the empty middle is kept).
Why is if s.find("x"): unsafe?
A match at index 0 is falsy AND a missing -1 is truthy — both directions can be wrong; use != -1 or in.
Last valid start index searching length-4 sub in length-11 string?
11 - 4 = 7.
What does "{:.1%}".format(0.8736) give?
'87.4%' (×100, one decimal, percent sign).
Correct order to normalise messy text (mnemonic)?
SLuSH-J → Strip, Lower, Split, Join.
Does strip() on a whole CSV row clean every field?
No — only the two outer ends; strip each field after splitting.

Connections

  • Strings — indexing and slicing (the s[i:i+m] window inside find)
  • Lists (split output, join input)
  • Immutability in Python (why you reassign)
  • f-strings (modern alternative to .format)
  • Boolean truthiness (the find 0/-1 trap)
  • User input and validation (the cleaning pipelines above)
  • Parent: Hinglish note