Can you write this from memory?
Write an expression that splits `item` by commas into a list of strings
In Python, was it split() or split(" ")? How do you right-pad with f-strings? Why did rstrip(".txt") delete too much?
Python string methods are individually simple, but there are dozens of them, and the details blur together. These exercises focus on the patterns that actually trip people up: the split() whitespace behavior, the strip() character-set gotcha, f-string formatting syntax, and the join() TypeError.
Quick reference:
split()→ groups whitespace, strips endssplit(" ")→ literal space only, keeps emptiesstrip("abc")→ removes any of a, b, c (not the substring "abc")removeprefix("abc")→ removes exact prefix (Python 3.9+)f"{x:>10}"→ right-align in 10 chars
For a quick copy-paste reference of every method covered here, see the Python string methods cheat sheet.
The most common string confusion in Python:
"a b c".split() # ['a', 'b', 'c'] — whitespace grouped
"a b c".split(" ") # ['a', '', 'b', '', 'c'] — literal space
When sep is not specified (or is None):
- Runs of consecutive whitespace are treated as one separator
- Leading and trailing whitespace is stripped
- The result never has empty strings at the ends
When sep is specified:
- Each occurrence of the separator creates a split
- Consecutive separators create empty strings
- Leading/trailing separators create empty strings at the ends
" hello world ".split() # ['hello', 'world']
" hello world ".split(" ") # ['', '', 'hello', 'world', '', '']
Rule of thumb:
- Use
split()for human-written text with arbitrary whitespace - Use
split(sep)for structured data (CSV, logs) where empty fields matter - For multiple delimiters, use
re.split()
This trips up almost everyone:
"test.txt".rstrip(".txt") # "tes" — not "test"!
Why? strip(), lstrip(), and rstrip() treat the argument as a set of characters to remove from the ends. They keep removing any character that appears in the set until they hit one that doesn't.
"aabbcc".strip("abc") # "" — all chars are in the set!
"file.txt".rstrip(".txt") # "file" if lucky, less if unlucky
The fix: Use removeprefix() and removesuffix() (Python 3.9+):
"test.txt".removesuffix(".txt") # "test"
"test.txt".removeprefix("test") # ".txt"
For older Python, use slicing with a check:
s = "test.txt"
if s.endswith(".txt"):
s = s[:-4]
join() is the inverse of split(), but it has a strict requirement:
",".join(["a", "b", "c"]) # "a,b,c"
",".join([1, 2, 3]) # TypeError: sequence item 0: expected str instance, int found
The fix: Convert all items to strings first:
# Option 1: map (slightly faster)
",".join(map(str, items))
# Option 2: generator expression
",".join(str(x) for x in items)
# Option 3: list comprehension
",".join([str(x) for x in items])
This is intentional design—Python prefers explicit conversion over surprising coercion.
f-strings use a powerful formatting syntax after the colon:
f"{value:[[fill]align][width][,][.precision][type]}"
Alignment
| Spec | Meaning | Example |
|---|---|---|
< | Left-align | f"{x:<10}" |
> | Right-align | f"{x:>10}" |
^ | Center | f"{x:^10}" |
Add a fill character before the alignment:
f"{'hi':_<10}" # "hi________"
f"{'hi':_>10}" # "________hi"
f"{'hi':_^10}" # "____hi____"
Number formatting
n = 1234567
f"{n:,}" # "1,234,567" — thousands separator
f"{n:_}" # "1_234_567" — underscore separator
n = 42
f"{n:05d}" # "00042" — zero-pad to 5 digits
f"{n:+d}" # "+42" — always show sign
x = 3.14159
f"{x:.2f}" # "3.14" — 2 decimal places
f"{x:10.2f}" # " 3.14" — width 10, 2 decimals
Debug syntax (Python 3.8+)
The = specifier prints both the expression and its value:
x = 42
print(f"{x=}") # x=42
print(f"{x = }") # x = 42 (spaces preserved)
print(f"{x*2 = }") # x*2 = 84 (works with expressions)
print(f"{x = :.2f}") # x = 42.00 (with format spec)
This replaces the common pattern of writing f"x = {x}" for debugging.
String slicing uses the same syntax as lists:
s = "Python"
s[0:3] # "Pyt" — indices 0, 1, 2
s[3:] # "hon" — from index 3 to end
s[:3] # "Pyt" — from start to index 3
s[::2] # "Pto" — every 2nd character
s[::-1] # "nohtyP" — reversed
Key insight: Slicing never raises IndexError. Out-of-bounds indices are silently clamped:
s = "hi"
s[0:100] # "hi" — not an error
s[100:] # "" — empty string
s[5] # IndexError! — indexing does raise
Reverse a string:
"hello"[::-1] # "olleh"
The -1 step means "go backwards by 1".
split("\n") has two problems:
- It doesn't handle Windows line endings (
\r\n) - A trailing newline creates an empty string at the end
"a\nb\n".split("\n") # ['a', 'b', '']
"a\nb\n".splitlines() # ['a', 'b']
"a\r\nb".split("\n") # ['a\r', 'b'] — \r left behind
"a\r\nb".splitlines() # ['a', 'b'] — handles all endings
splitlines() recognizes: \n, \r\n, \r, and several others.
Pass keepends=True if you need the line endings:
"a\nb".splitlines(keepends=True) # ['a\n', 'b']
"Does this string contain X?" is one of the most common string operations.
The in operator (existence check)
if "cat" in "concatenate":
print("Found it!") # This runs
Use in when you only need yes/no. It's readable and fast.
find() vs index()
Both return the position of a substring:
"hello".find("ll") # 2
"hello".index("ll") # 2
"hello".find("x") # -1 (not found)
"hello".index("x") # ValueError: substring not found
When to use which:
find(): when "not found" is a valid case you'll handleindex(): when "not found" should crash (it's a bug)
# find() pattern: check the return value
pos = text.find(target)
if pos != -1:
# Found at position pos
...
# index() pattern: let it crash if missing
pos = text.index(target) # Bug if not found
Right-to-left: rfind() and rindex() search from the end.
For validating user input:
| Method | True for | False for |
|---|---|---|
isdigit() | "123" | "12.3", "12a", "" |
isalpha() | "abc", "абв" | "abc1", "a b", "" |
isalnum() | "abc123" | "abc 123", "" |
isnumeric() | "123", "½", "²" | "12.3", "" |
isspace() | " \t\n" | "a ", "" |
user_input = input("Enter age: ")
if user_input.isdigit():
age = int(user_input)
else:
print("Please enter a number")
Note: All return False for empty strings. isnumeric() is broader than isdigit()—it includes Unicode numerals like ½ and ².
Backslashes are escape characters in normal strings:
print("Line1\nLine2") # Prints on two lines
print("C:\\Users\\name") # Need double backslashes
Raw strings treat backslashes as literal:
print(r"Line1\nLine2") # Prints: Line1\nLine2
print(r"C:\Users\name") # Prints: C:\Users\name
Use raw strings for:
-
Regex patterns:
\dmeans "digit" in regex, but Python sees\das an escape. For a deeper look at regex syntax, see the Python regex cheat sheet.import re re.findall(r"\d+", "abc123") # ['123'] re.findall("\\d+", "abc123") # Same, but ugly -
Windows paths:
path = r"C:\Users\name\Documents"
Gotcha: Raw strings can't end with an odd number of backslashes (use regular string or add space).
For ASCII text, lower() works fine:
"Hello".lower() == "hello" # True
For international text, use casefold():
# German sharp S
"ß".lower() # "ß" — still sharp S
"ß".casefold() # "ss" — proper lowercase
# Case-insensitive comparison
"STRASSE".casefold() == "straße".casefold() # True
"STRASSE".lower() == "straße".lower() # False!
Rule: Use casefold() for case-insensitive comparison of user input. Use lower() when you specifically want ASCII-only lowercasing. If you work across languages, Python vs JavaScript string handling covers the key differences in how each language approaches string operations.
TypeError: sequence item 0: expected str instance, int found
From join() with non-strings. Fix: ",".join(map(str, items))
IndexError: string index out of range
From indexing (not slicing) beyond the string length. Fix: check len() first, or use slicing which doesn't raise.
TypeError: 'str' object does not support item assignment
Strings are immutable. You can't do s[0] = "X". Fix: build a new string with slicing or replace().
Clean and split CSV-like input
text = " Alice, Bob ,Carol "
names = [s.strip() for s in text.split(",")]
# ['Alice', 'Bob', 'Carol']
Check multiple extensions
if filename.endswith((".jpg", ".png", ".gif")):
process_image(filename)
Title case with exceptions
s = "the quick BROWN fox"
s.title() # "The Quick Brown Fox" — capitalizes every word
s.capitalize() # "The quick brown fox" — first char only
Multiline f-strings
# Use parentheses for implicit concatenation
message = (
f"Hello {name}, "
f"you have {count} items "
f"worth {total:.2f}"
)
# Or triple quotes for literal newlines
message = f"""
Hello {name},
You have {count} items.
"""
Replace with count limit
# Replace all occurrences (default)
"a-b-c-d".replace("-", "_") # "a_b_c_d"
# Replace only first N occurrences
"a-b-c-d".replace("-", "_", 2) # "a_b_c-d"
Replace multiple characters
# For a few characters, chain replace:
s.replace("a", "").replace("b", "").replace("c", "")
# For many, use str.translate:
s.translate(str.maketrans("", "", "abc"))
When to Use Python String Methods: f-strings, split, join, strip, slicing
- Format dynamic output with f-strings—interpolation, padding, alignment, number formatting.
- Parse and clean user input with split/strip/replace.
- Build strings from sequences with join (remember: all items must be strings).
- Slice for substrings, including reverse with [::-1].
- Use splitlines() for cross-platform newline handling.
Check Your Understanding: Python String Methods: f-strings, split, join, strip, slicing
Given " Alice, Bob ,Carol ", return ["Alice", "Bob", "Carol"].
Use split(",") and strip() on each piece: `[s.strip() for s in text.split(",")]`. Note: split(",") keeps the spaces; strip() on each item removes them. Don't use split() without args here—it would split on spaces too.
What You'll Practice: Python String Methods: f-strings, split, join, strip, slicing
Common Python String Methods: f-strings, split, join, strip, slicing Pitfalls
- Strings are immutable—methods return new strings, they don't modify in place
- Index out of range errors—use slicing (never raises) or check len() first
- Forgetting to call the method—`s.lower` vs `s.lower()`
- strip() treats arg as character set, not substring—use removeprefix/removesuffix
- join() requires all strings—use map(str, items) for mixed types
- split() vs split(" ")—different whitespace handling (see FAQ)
- split("\n") leaves empty string at end—use splitlines() instead
Python String Methods: f-strings, split, join, strip, slicing FAQ
Why does split() behave differently with no argument?
`split()` (no args) treats runs of whitespace as a single separator AND strips leading/trailing whitespace. `split(" ")` splits on literal spaces only—consecutive spaces create empty strings, and leading/trailing spaces create empty items. Use `split()` for whitespace-separated words; use `split(sep)` for structured data like CSV.
Why doesn't rstrip(".txt") work like I expect?
`strip()`/`lstrip()`/`rstrip()` treat the argument as a **set of characters**, not a substring. `rstrip(".txt")` removes any trailing t, x, or . characters. For actual prefix/suffix removal, use `removeprefix()` / `removesuffix()` (Python 3.9+), or slice with `endswith()` check.
How do I pad/align with f-strings?
Use the format spec after a colon: `f"{x:>10}"` right-aligns in 10 chars, `f"{x:<10}"` left-aligns, `f"{x:^10}"` centers. Add a fill character before the alignment: `f"{x:_>10}"` pads with underscores. For numbers: `f"{n:05d}"` zero-pads to 5 digits, `f"{n:,}"` adds thousand separators.
Why does join() give TypeError: expected str instance, int found?
`join()` requires all items to be strings. Fix: `",".join(map(str, items))` or `",".join(str(x) for x in items)`. This is intentional—explicit conversion prevents surprising string coercion.
Are strings mutable in Python?
No. Methods like replace() or lower() return new strings rather than modifying in place. This means you need to reassign: `s = s.lower()`, not just `s.lower()`.
When should I use f-strings vs .format()?
Use f-strings for most cases—they're faster and more readable. Use `.format()` when: (1) the template is stored separately from values, (2) you need to reuse a template with different values, or (3) you're on Python < 3.6. Both use the same underlying format spec syntax.
What's f"{var=}" (the debug syntax)?
Python 3.8+ lets you write `f"{x=}"` which prints both the name and value: `x=42`. Great for quick debugging. You can add format specs too: `f"{x=:.2f}"`. Spaces around `=` are preserved: `f"{x = }"` prints `x = 42`.
When should I use splitlines() vs split("\n")?
`splitlines()` handles all line-ending styles (\n, \r\n, \r) and doesn't leave an empty string at the end. `split("\n")` only splits on \n and creates empty strings from trailing newlines. Use `splitlines()` for text files; use `split("\n")` only when you specifically want \n behavior.
How do I reverse a string?
Use slice notation: `s[::-1]`. The -1 step traverses backwards. This works because strings are sequences in Python.
How do I check if a string contains a substring?
Use the `in` operator: `if "cat" in text:`. For the position, use `find()` (returns -1 if not found) or `index()` (raises ValueError if not found). Prefer `in` for existence checks, `find()` when you need the position and missing is expected, `index()` when missing is a bug.
What's the difference between find() and index()?
Both return the position of a substring. `find()` returns -1 if not found; `index()` raises ValueError. Use `find()` when missing is a valid case (check `!= -1`), `index()` when missing should crash. Same pattern: `rfind()`/`rindex()` search from the right.
How do I check if a string is all digits/letters?
`isdigit()` checks for digits (0-9). `isalpha()` checks for letters. `isalnum()` checks for both. `isnumeric()` is broader than `isdigit()` (includes ½, ²). For validating integer input, `isdigit()` is usually what you want. All return False for empty strings.
When should I use raw strings (r"...")?
Use raw strings when backslashes should be literal: regex patterns (`r"\d+"`) and Windows paths (`r"C:\Users\name"`). In raw strings, `\` is just a backslash, not an escape. Without `r`, you'd need `"\\d+"` for the same regex.
What's the difference between lower() and casefold()?
`lower()` lowercases ASCII letters. `casefold()` is more aggressive—it handles international characters like German ß → ss. Use `casefold()` for case-insensitive comparison of user input that might contain non-ASCII text.
Python String Methods: f-strings, split, join, strip, slicing Syntax Quick Reference
name = "Alice"
greeting = f"Hello, {name}!"f"{x:>10}" # Right-align in 10 chars
f"{x:<10}" # Left-align
f"{x:^10}" # Center
f"{x:_>10}" # Pad with underscoresf"{n:05d}" # Zero-pad to 5 digits
f"{n:,}" # Thousands separator: 1,234,567
f"{x:.2f}" # Two decimal places: 3.14x = 42
print(f"{x=}") # x=42
print(f"{x = }") # x = 42
print(f"{x*2 = }") # x*2 = 84"a b c".split() # ['a', 'b', 'c']
"a b c".split(" ") # ['a', '', 'b', '', 'c']"test.txt".rstrip(".txt") # "tes" (oops!)
"test.txt".removesuffix(".txt") # "test" (correct)nums = [1, 2, 3]
",".join(map(str, nums)) # "1,2,3"text = "Python"
text[:3] # "Pyt"
text[3:] # "hon"
text[::-1] # "nohtyP""a\nb\n".split("\n") # ['a', 'b', '']
"a\nb\n".splitlines() # ['a', 'b']filename.endswith((".jpg", ".png", ".gif"))"cat" in "concatenate" # True
"hello".find("ll") # 2
"hello".find("x") # -1 (not found)
"hello".index("x") # ValueError!"123".isdigit() # True
"abc".isalpha() # True
"abc123".isalnum() # True
"".isdigit() # False (empty)r"\d+" # Regex: literal \d+
r"C:\Users\name" # Windows path"ß".lower() # "ß"
"ß".casefold() # "ss" (for comparison)"a-b-c-d".replace("-", "_", 2) # "a_b_c-d"Python String Methods: f-strings, split, join, strip, slicing Sample Exercises
Write an expression that slices `item` from index `` up to (but not including) ``
item[:]Write an expression that gets the character at index `` from `item`
item[]Write a slice that takes every other character from `item`, starting at index ``
item[::2]+ 50 more exercises
Copy-ready syntax examples for quick lookup