Session 11 of the Python Fundamentals series, covered in depth. It cleans and shapes text so that messy user input behaves, covering .lower() and .upper(), .strip(), the in keyword for substrings, .startswith() and .endswith(), length, indexing and slicing, .split() and .replace(), and f-strings with formatting. It builds the robust clean-then-compare pattern and a word-count text analyzer, and covers the key traps: strings are immutable, string methods return a new string, .strip() trims only the ends, and comparisons are case-sensitive. Every snippet and error message was copied verbatim from CPython 3.12.
Subject: Python Fundamentals · 95 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
Python Fundamentals - Session 11
Clean and shape text so messy input behaves
Objectives
input() gives strings, and real users type inconsistently. This session is the toolkit for handling that. By the end you can:
.lower(), .upper(), and .strip().in, .startswith(), and .endswith()..split() and swap parts with .replace().Warm-up
Discussion prompt
Before we open Session 11 - Strings Toolkit: without looking back, what was the main idea of Session 10 - Functions, Deeper (Scope & Design), and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
That session covers local scope, shadowing, one-job functions, composition, returning multiple values, a menu program, and the difference between changing a value in place and rebinding it.
Section
Part 1
Concept
One user types yes, another YES, another Yes . All mean the same thing - but to Python they are three different strings.
Your job is to normalize the text so all of these count as the same answer.
Counterexample
Discussion prompt
One user types yes, another YES, another Yes . All mean the same thing - but to Python they are three different strings.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Answer:
Your job is to normalize the text so all of these count as the same answer.
Intuition
Good programs meet people where they are. Rather than demand perfect input, clean it up: trim the spaces, even out the case, then compare.
A few string methods turn brittle checks into forgiving ones.
Analogy
Discussion prompt
Explain Be forgiving of the user by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Good programs meet people where they are. Rather than demand perfect input, clean it up: trim the spaces, even out the case, then compare.
Concept
"Hello" is really the ordered characters H, e, l, l, o. You can measure it, index into it, and slice pieces out.
That is why many list ideas - len, indexing from 0, in - work on strings too.
Explain it
Discussion prompt
Explain A string is a sequence of characters to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
"Hello" is really the ordered characters H, e, l, l, o. You can measure it, index into it, and slice pieces out.
Section
Part 2
Concept
text.lower() gives an all-lowercase copy; text.upper() an all-uppercase one. Attach the method with a dot.
Socratic
Discussion prompt
text.lower() gives an all-lowercase copy; text.upper() an all-uppercase one. Attach the method with a dot.
Suppose that were not true. What is the first thing in Session 11 - Strings Toolkit that would stop working?
Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.
Worked example
print("HELLO".lower())
print("hi".upper())Each method returns a new-cased copy
Why: lower makes hello; upper makes HI.
Read the output
Why: Verified by execution.
| expression | value |
|---|---|
| "HELLO".lower() | hello |
| "hi".upper() | HI |
Comparison
Comparison matrix
From Evening out the case: refill the value column from what you know. The rest of the table is as it appeared.
| expression | value |
|---|---|
| "HELLO".lower() | hello |
| "hi".upper() | HI |
Concept
String methods do not change the original - they hand back a new string. Strings themselves never change (more on that soon).
So you must use or store the result: text = text.lower().
Fill the middle
Fill in the blanks
From The original is unchanged — one line has had its right-hand side removed. Put it back.
word = "Hello"
word.lower()
print(word)
Why: word is what everything below it consumes, so the wrong expression here fails later and somewhere else. The lowercase copy is not stored anywhere, so word is untouched.
Worked example
word = "Hello"
word.lower()
print(word)word.lower() makes a copy that is thrown away
Why: The lowercase copy is not stored anywhere, so word is untouched.
Read the output
Why: Verified by execution: prints Hello (still capitalized). You needed word = word.lower().
| line | word |
|---|---|
| 2 | Hello (copy discarded) |
| 3 | Hello |
Trade off
Comparison matrix
From The original is unchanged: every row here is a choice with a cost. Fill the word column, then say which row you would actually pick and what you give up for it.
| line | word |
|---|---|
| 2 | Hello (copy discarded) |
| 3 | Hello |
Anomaly
Predict first
A student writes this, and it looks reasonable:
Comparing raw input to "yes". The user types Yes.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: "Yes" is not "yes" - the capital Y differs.
Lowercase first, then compare.
Why: "Yes" is not "yes" - the capital Y differs. So a perfectly reasonable answer is rejected.
Trap
Comparing raw input to "yes". The user types Yes.
answer = "Yes"
print(answer == "yes")Different case is unequal
Why: "Yes" is not "yes" - the capital Y differs. So a perfectly reasonable answer is rejected.
| expression | value |
|---|---|
| "Yes" == "yes" | False |
Lowercase first, then compare.
answer = "Yes"
print(answer.lower() == "yes").lower() makes the comparison case-blind
Why: answer.lower() is "yes", which matches. Real output: True. Now Yes, YES, and yes all pass.
| expression | value |
|---|---|
| "Yes".lower() == "yes" | True |
Concept
.title() capitalizes the first letter of each word ("ana lee".title() is "Ana Lee"), and .capitalize() capitalizes just the first letter of the whole string.
Like .lower(), these return a new string - use them mostly for display, and stick to .lower() when comparing.
Section
Part 3
Concept
.strip() removes spaces (and tabs/newlines) from the start and end of a string - great for cleaning up stray input.
Explain it to yourself
Discussion prompt
In Trimming the ends this move is made:
Read the output
Why is that legal? Name the rule or definition it rests on before you read on.
Hint: If you can only say "because that is what you do", the rule is the thing to go and find.
Answer:
Verified by execution: Hello, then 5 (the spaces are gone).
Worked example
s = " Hello "
print(s.strip())
print(len(s.strip()))Leading and trailing spaces go
Why: The word Hello remains, with no surrounding spaces.
Read the output
Why: Verified by execution: Hello, then 5 (the spaces are gone).
| expression | value |
|---|---|
| s.strip() | Hello |
| len(s.strip()) | 5 |
Error analysis
Annotate
Walk the callouts on Trimming the ends. Each one is a place this is easy to get subtly wrong.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Expecting strip to remove ALL spaces.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: The space between a and b is in the middle, so strip leaves it.
To remove inner spaces too, replace them.
Why: The space between a and b is in the middle, so strip leaves it. You get "a b", not "ab".
Trap
Expecting strip to remove ALL spaces.
s = " a b "
print(s.strip())Only the edges are trimmed
Why: The space between a and b is in the middle, so strip leaves it. You get "a b", not "ab".
| input | s.strip() |
|---|---|
| " a b " | "a b" |
To remove inner spaces too, replace them.
s = " a b "
print(s.strip().replace(" ", ""))strip the ends, replace removes the rest
Why: .replace(" ", "") deletes every remaining space. Real output: ab. Pick the tool for where the spaces are.
| step | value |
|---|---|
| strip() | "a b" |
| replace(" ", "") | "ab" |
Concept
Chain them: text.strip().lower() trims the ends and evens the case in one step - the standard way to normalize an answer.
Read left to right: strip first, then lower the result.
Sorting
Sort into buckets
These are the pieces of Session 11 - Strings Toolkit, out of order. Put each one back under the part of the lesson it belongs to.
Pattern
Predict first
The table runs: answer.strip() | "YES" · ...lower() | "yes"
In A robust yes check, given the rows so far: what is the next one — the row where step is == "yes"?
Correct: == "yes" | True
| step | value |
|---|---|
| answer.strip() | "YES" |
| ...lower() | "yes" |
| == "yes" | True |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. " YES " becomes "yes" after strip and lower, which matches.
Worked example
answer = " YES "
if answer.strip().lower() == "yes":
print("confirmed")Clean, then compare
Why: " YES " becomes "yes" after strip and lower, which matches.
Read the output
Why: Verified by execution: prints confirmed. Now spaces and caps no longer matter.
| step | value |
|---|---|
| answer.strip() | "YES" |
| ...lower() | "yes" |
| == "yes" | True |
Pattern
Step through it
Step through A robust yes check one row at a time. What is driving the change, and what would the row after the last one be?
Section
Part 4
Concept
"ell" in "Hello" is True if that piece appears anywhere in the string. It is the substring test.
Worked example
print("ell" in "Hello")
print("xyz" in "Hello")in scans the whole string
Why: ell appears inside Hello; xyz does not.
Read the output
Why: Verified by execution: True then False.
| expression | value |
|---|---|
| "ell" in "Hello" | True |
| "xyz" in "Hello" | False |
Blank canvas
Draw it
Draw what Is it in there? just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Concept
Combine cleaning with in to catch a keyword no matter the case or surrounding words: "bye" in message.lower().
This upgrades a chatbot from exact-match to 'reacts if the word appears at all'.
Fill the middle
Fill in the blanks
From A keyword-spotting bot — one line has had its right-hand side removed. Put it back.
message = "Ok BYE now"
if "bye" in message.lower():
print("Goodbye!")
Why: message is what everything below it consumes, so the wrong expression here fails later and somewhere else. message.lower() is "ok bye now", which contains "bye".
Worked example
message = "Ok BYE now"
if "bye" in message.lower():
print("Goodbye!")Lowercase, then look for the keyword
Why: message.lower() is "ok bye now", which contains "bye".
Read the output
Why: Verified by execution: prints Goodbye! - even though bye was capitalized and mid-sentence.
| message.lower() | "bye" in it? |
|---|---|
| ok bye now | True |
Error analysis
Annotate
Walk the callouts on A keyword-spotting bot. Each one is a place this is easy to get subtly wrong.
Concept
text.startswith("http") and text.endswith(".py") check the beginning or end of a string - returning a bool.
Worked example
name = "report.py"
print(name.endswith(".py"))
print(name.startswith("report"))Check the ends
Why: It ends with .py and starts with report.
Read the output
Why: Verified by execution: True then True.
| call | value |
|---|---|
| endswith(".py") | True |
| startswith("report") | True |
Section
Part 5
Concept
len("Hello") is 5 - the number of characters, spaces included.
Worked example
print(len("Hello"))
print(len("a b"))Every character counts, spaces too
Why: Hello is 5; "a b" is 3 (a, space, b).
Read the output
Why: Verified by execution: 5 then 3.
| string | len |
|---|---|
| Hello | 5 |
| a b | 3 |
Comparison
Comparison matrix
From How long is it?: refill the len column from what you know. The rest of the table is as it appeared.
| string | len |
|---|---|
| Hello | 5 |
| a b | 3 |
Concept
"Hello"[0] is the first character, "H". Just like lists, indexing starts at 0.
Worked example
word = "Hello"
print(word[0])
print(word[-1])0 is first, -1 is last
Why: word[0] is H; word[-1] is o.
Read the output
Why: Verified by execution: H then o.
| index | char |
|---|---|
| 0 | H |
| -1 | o |
Concept
"Hello"[0:3] takes characters at index 0, 1, 2 - "Hel". The end index is excluded, like range.
Leave a side blank to go to the edge: word[1:] is everything from index 1 on.
Worked example
word = "Hello"
print(word[0:3])
print(word[1:])0:3 is indexes 0, 1, 2
Why: The character at index 3 is excluded.
1: goes to the end
Why: Verified by execution: Hel then ello.
| slice | value |
|---|---|
| word[0:3] | Hel |
| word[1:] | ello |
Anomaly
Predict first
A student writes this, and it looks reasonable:
Trying to change one character in place.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Strings are immutable - they never change once made.
Build a new string instead.
Why: Strings are immutable - they never change once made. Assigning to s[0] raises a TypeError.
Trap
Trying to change one character in place.
s = "hi"
s[0] = "H"You cannot assign to a character
Why: Strings are immutable - they never change once made. Assigning to s[0] raises a TypeError.
| you write | result |
|---|---|
| s[0] = "H" | TypeError: 'str' object does not support item assignment |
Build a new string instead.
s = "hi"
s = "H" + s[1:]
print(s)Make a fresh string and rebind s
Why: "H" + s[1:] is "H" + "i" = "Hi". Real output: Hi. To 'change' a string, create a new one.
| expression | value |
|---|---|
| "H" + s[1:] | Hi |
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
yes, another YES, another Yes . All mean the same thing - but to Python they are three different strings.; Good programs meet people where they are. Rather than demand perfect input, clean it up: trim the spaces, even out the case, then compare.; "Hello" is really the ordered characters H, e, l, l, o. You can measure it, index into it, and slice pieces out.Section
Part 6
Concept
text.split() with no argument splits on whitespace, giving a list of words.
This is the bridge from a sentence to something you can loop over or count.
Fill the middle
Fill in the blanks
From Sentence into words — one line has had its right-hand side removed. Put it back.
text = "one two three"
words = text.split()
print(words)
print(len(words))
Why: words is what everything below it consumes, so the wrong expression here fails later and somewhere else. Verified by execution: ['one', 'two', 'three'], then 3.
Worked example
text = "one two three"
words = text.split()
print(words)
print(len(words))split hands back a list
Why: Each run of spaces becomes a separator.
Now len counts words
Why: Verified by execution: ['one', 'two', 'three'], then 3.
| expression | value |
|---|---|
| text.split() | ['one', 'two', 'three'] |
| len(words) | 3 |
Concept
Pass a separator to split on it: "a,b,c".split(",") gives ['a', 'b', 'c'] - handy for CSV-style text.
Explain it to yourself
Discussion prompt
In Splitting a comma list this move is made:
Read the output
Why is that legal? Name the rule or definition it rests on before you read on.
Hint: If you can only say "because that is what you do", the rule is the thing to go and find.
Answer:
Verified by execution: ['apple', 'pear', 'plum'].
Worked example
csv = "apple,pear,plum"
print(csv.split(","))The comma is the separator
Why: Each comma marks a break between items.
Read the output
Why: Verified by execution: ['apple', 'pear', 'plum'].
| expression | value |
|---|---|
| csv.split(",") | ['apple', 'pear', 'plum'] |
Concept
text.replace(old, new) returns a copy with every old swapped for new.
Like other methods, it returns a new string - store it if you want to keep the change.
Socratic
Discussion prompt
text.replace(old, new) returns a copy with every old swapped for new.
Suppose that were not true. What is the first thing in Session 11 - Strings Toolkit that would stop working?
Hint: Follow it one step downstream. The answer is whatever was quietly relying on it.
Answer:
Like other methods, it returns a new string - store it if you want to keep the change.
Worked example
s = "I like cats"
print(s.replace("cats", "dogs"))Every match is replaced
Why: cats becomes dogs.
Read the output
Why: Verified by execution: I like dogs.
| expression | value |
|---|---|
| s.replace("cats", "dogs") | I like dogs |
Section
Part 7
Concept
Put f before the quotes and wrap each value in { }. Cleaner than gluing with +, and it converts values for you.
Explain it
Discussion prompt
Explain f-strings drop values into text to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
Put f before the quotes and wrap each value in { }. Cleaner than gluing with +, and it converts values for you.
Worked example
name = "Ana"
score = 3
print(f"{name} scored {score}")Each brace is filled with a value
Why: {name} -> Ana, {score} -> 3.
Read the output
Why: Verified by execution: Ana scored 3.
| blank | fills with |
|---|---|
| {name} | Ana |
| {score} | 3 |
Concept
Add :.2f for two decimals, or :>8 to right-align in a field - useful for tidy tables and money.
Analogy
Discussion prompt
Explain Format numbers inside braces by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Add :.2f for two decimals, or :>8 to right-align in a field - useful for tidy tables and money.
Worked example
price = 4.5
print(f"Total: ${price:.2f}"):.2f shows two decimal places
Why: 4.5 displays as 4.50.
Read the output
Why: Verified by execution: Total: $4.50.
| value | shown as |
|---|---|
| 4.5 | $4.50 |
Blank canvas
Draw it
Draw what Two-decimal money just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Section
Part 8
Concept
len(text.split()) is the word count: split into a list of words, then count the list.
Counterexample
Discussion prompt
len(text.split()) is the word count: split into a list of words, then count the list.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Explain it to yourself
Discussion prompt
In Words and a keyword this move is made:
in checks for a keyword
Why is that legal? Name the rule or definition it rests on before you read on.
Hint: If you can only say "because that is what you do", the rule is the thing to go and find.
Answer:
Verified by execution: words: 4, then has 'fox': True.
Worked example
text = "the quick brown fox"
print("words:", len(text.split()))
print("has 'fox':", "fox" in text.lower())split then len counts words
Why: Four words in the sentence.
in checks for a keyword
Why: Verified by execution: words: 4, then has 'fox': True.
| measure | value |
|---|---|
| word count | 4 |
| contains fox | True |
Trade off
Comparison matrix
From Words and a keyword: every row here is a choice with a cost. Fill the value column, then say which row you would actually pick and what you give up for it.
| measure | value |
|---|---|
| word count | 4 |
| contains fox | True |
Estimation
Predict first
The user types Paris (extra spaces, mixed case can vary):
Commit before you compute: what does Normalize then compare come out to? A rough magnitude and the right form is enough — the point is to have something concrete to be wrong about.
Correct: Read the output
Why: A prediction you can defend turns the computation into a check rather than a leap of faith — and an answer that contradicts it is caught on the spot. Verified by execution: prints correct - forgiving of spaces and case.
Worked example
The user types Paris (extra spaces, mixed case can vary):
guess = " Paris "
if guess.strip().lower() == "paris":
print("correct")Clean the guess, then compare to the lowercase answer
Why: strip and lower make "paris", which matches.
Read the output
Why: Verified by execution: prints correct - forgiving of spaces and case.
| step | value |
|---|---|
| guess.strip() | "Paris" |
| ...lower() | "paris" |
| == "paris" | True |
Comparison
Comparison matrix
From Normalize then compare: refill the value column from what you know. The rest of the table is as it appeared.
| step | value |
|---|---|
| guess.strip() | "Paris" |
| ...lower() | "paris" |
| == "paris" | True |
Section
Part 9
Pattern
1. Normalize: text.strip().lower()
Why: Trims stray spaces and evens the case so comparisons are forgiving.
2. Match: == for exact, in for 'contains'
Why: cleaned == "yes" for a whole answer; keyword in cleaned to spot a word anywhere.
3. Remember methods return a NEW string
Why: text = text.lower(); the original never changes on its own.
4. Split to count or loop; f-strings to display
Why: len(text.split()) counts words; f"{x}" builds clean output.
Explain it to yourself
Discussion prompt
In String toolkit at a glance this move is made:
Shape: len, [i], [a:b], .split(), .replace()
Why is that legal? Name the rule or definition it rests on before you read on.
Hint: If you can only say "because that is what you do", the rule is the thing to go and find.
Answer:
Measure, index, slice, break apart, and swap - strings are immutable, so these return new strings.
Pattern
Case: .lower() / .upper()
Why: Even out capitalization before comparing.
Trim: .strip(); search: in, .startswith, .endswith
Why: strip trims the ends only; in tests a substring anywhere.
Shape: len, [i], [a:b], .split(), .replace()
Why: Measure, index, slice, break apart, and swap - strings are immutable, so these return new strings.
Real world
Discussion prompt
Outside this lesson: where does Session 11 - Strings Toolkit actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of String toolkit at a glance is doing the work in it.
Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.
Answer:
Session 11 of the Python Fundamentals series, in depth. Cleaning and shaping text so messy user input behaves: .lower()/.upper(), .strip(), the in keyword for substrings, .startswith()/.endswith(), length, indexing and slicing, .split() and .replace(), and f-strings with formatting.
Check
Was word changed?
word = "Hi"
word.upper()
print(word)| stored? | word |
|---|---|
| no | ? |
Check your understanding
What does this print?
Answer: A
Why: word.upper() returns a new uppercase string but it is not stored, so word is unchanged. You would need word = word.upper(). Verified by execution.
Check
The answer has spaces and caps.
a = " YES "
print(a.strip().lower() == "yes")| cleaned | == yes |
|---|---|
| ? | ? |
Check your understanding
What does this print?
Answer: A
Why: strip removes the spaces and lower makes it "yes", which equals "yes", so the result is True. Verified by execution.
Check
Which characters?
s = "Python"
print(s[0:3])| index | 0,1,2 |
|---|---|
| chars | ? |
Check your understanding
What does this print?
Answer: A
Why: s[0:3] takes indexes 0, 1, 2 (P, y, t) and excludes index 3, giving Pyt. Verified by execution.
Check
Does the piece appear?
print("cat" in "category")| expression | value |
|---|---|
| "cat" in "category" | ? |
Check your understanding
What does this print?
Answer: A
Why: cat appears at the start of category, so the substring test is True. Verified by execution.
Check
How many words?
text = "a b c d"
print(len(text.split()))| split() | len |
|---|---|
| ? | ? |
Check your understanding
What does this print?
Answer: A
Why: split() breaks on spaces into ['a', 'b', 'c', 'd'], and len of that is 4. Verified by execution.
Check
Can you edit one character?
s = "cat"
s[0] = "b"| assign s[0]? | result |
|---|---|
| ? | ? |
Check your understanding
What happens?
Answer: A
Why: Strings cannot be changed in place, so assigning to s[0] raises a TypeError. Build a new string: s = "b" + s[1:]. Verified by execution.
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — Messy Text, Clean Code · Changing Case · Trimming Whitespace · Searching Inside Text · Length, Index, Slice · Splitting & Replacing. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
Normalize with .strip().lower(), search with in / .startswith / .endswith, and shape with len, slicing, .split(), .replace(), and f-strings.
| You want | Use |
|---|---|
| case-blind compare | a.strip().lower() == b |
| keyword anywhere | word in text.lower() |
| a piece of the text | text[a:b] |
| words as a list | text.split() |
| swap text | text.replace(old, new) |
Remember: strings are immutable, and methods return a new string. Next session we stop fearing red error text - reading tracebacks and handling errors with try / except.
Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.