Python strings come with more than forty methods, and you will end up using about eight of them in nearly every program. The problem is that several of those eight have a behaviour that is not what the name suggests, and the traps only bite once. This lesson covers the daily drivers properly: cleaning whitespace, splitting text, changing case, testing prefixes, and the family of is predicates that decide whether a string is a number.
How do you clean up whitespace in a string?
Three methods handle the edges: strip() removes whitespace from both ends, lstrip() from the left and rstrip() from the right. The name is slightly misleading, because "strip" can also mean "remove these specific characters", and when you pass an argument it removes every occurrence of any of those characters from both ends rather than removing a prefix or suffix.
raw = ' \tTutorsbot \n'
print('raw ->', repr(raw))
print('stripped ->', repr(raw.strip()))
print('left only ->', repr(raw.lstrip()))
print('right only ->', repr(raw.rstrip()))
print('strip set ->', repr('xyxTutorsbotxy'.strip('xy')))
print('split ->', 'a b \t c'.split())
print('split sep ->', 'a, b,, c'.split(','))
print('split max ->', 'a, b,, c'.split(',', 1))
Look at the two splitting results. 'a b \t c'.split() without a separator splits on runs of whitespace and drops empty pieces, which is exactly what you want for parsing a line of text. 'a, b,, c'.split(',') splits on every comma and keeps the empty piece in the middle, which is usually a bug in the data rather than in the code.
Both behaviours are worth knowing deliberately. Splitting with no argument is the forgiving one; splitting with an explicit separator is the literal one, and it will happily hand you empty strings you then have to filter.
How do you change the case of a string?
Six methods cover case handling, and one of them is a trap. upper() and lower() are obvious. capitalize() uppercases the first character and lowercases the rest. swapcase() flips every character's case. casefold() is the aggressive one, and it is the right tool for comparing text that might contain accents.
name = 'ada lovelace'
print('upper ->', name.upper())
print('lower ->', name.lower())
print('title ->', name.title())
print('capitalize ->', name.capitalize())
print('swapcase ->', 'AbC'.swapcase())
print("title trap ->", "don't".title())
print('casefold ->', ascii('Straße'.casefold()), '| lower ->', ascii('Straße'.lower()))
The trap is title(). It treats every non-letter as a word boundary, which means an apostrophe starts a new word: "don't" becomes "Don'T". It also produces "Ada Lovelace" from "ada lovelace", which is what you want for a display name and is wrong for a sentence. If you need sentence case, take the first character and the rest separately rather than reaching for title().
The last line shows why casefold() exists. German sharp s lowercases to 'ß' but casefolds to 'ss', which is the form you want when comparing two strings for equality. Use lower() for display and casefold() for comparison.
Which checks and predicates save you the most time?
A small group of methods answers questions so often that you should reach for them by reflex. startswith() and endswith() test the edges of a string, and both accept a tuple so you can check several possibilities at once. removeprefix() and removesuffix(), added in Python 3.9, strip a known prefix or suffix and return the string unchanged when it is not there. partition() splits a string into three parts around the first occurrence of a separator.
filename = 'report-final.pdf'
print('startswith ->', filename.startswith('report'))
print('endswith ->', filename.endswith('.pdf'))
print('tuple form ->', filename.startswith(('report', 'summary')))
print('removeprefix ->', filename.removeprefix('report-'))
print('removesuffix ->', filename.removesuffix('.pdf'))
print('no match ->', filename.removeprefix('other-'))
head, separator, tail = 'key=value=extra'.partition('=')
print('partition ->', head, separator, tail)
Then there is the family of predicates that answer questions about content. isalpha(), isdigit(), isalnum(), isspace(), isupper(), islower() and istitle() each return a boolean, and all of them return False for an empty string rather than raising an error.
samples = ['abc', '123', '1.5', '', ' ', '\u00b2', '\u0663']
for text in samples:
print(
f'{ascii(text):<8} isalpha={text.isalpha()} isdigit={text.isdigit()} '
f'isalnum={text.isalnum()} isspace={text.isspace()}'
)
Note that '1.5' is neither alphabetic nor a digit, because of the decimal point, and that '²', a superscript two, counts as a digit. That last one matters if you validate a PIN: isdigit() accepts characters that int() will refuse to convert, so a form can pass your check and then fail at the conversion step.
What should you reach for instead of string methods?
String methods handle structure. Patterns need the re module, which the next lesson covers in detail. Structured data needs the json and csv modules, which the lesson after that covers. Knowing which problem you have saves you from writing a regular expression when a simple split would do, or from hand-parsing JSON with find() when a single json.loads() is both shorter and correct.
Two more habits keep string handling tidy. Reach for str.replace() when you want every occurrence replaced, and for the re module when you need a pattern or a single replacement. And when you build output, reach for f-strings rather than chaining replace() calls to reshape a template.
There is one more method worth knowing for cleanup work. str.translate() takes a translation table and maps every character through it, which is how you delete a known set of characters in one pass instead of chaining replace() calls. The table comes from str.maketrans(), which accepts the characters to delete and what to replace them with.
noisy = 'Order #42 (paid) - 2026/10/27!'
remove_punctuation = str.maketrans('', '', '()#-/')
print('cleaned ->', noisy.translate(remove_punctuation))
digits_to_zero = str.maketrans('0123456789', '##########')
print('masked -> ' + 'a1b2c3'.translate(digits_to_zero))
Compare that with the alternative of chaining five replace() calls, which produces the same result and reads worse. The translation table is also faster, because the string is walked once instead of once per rule.
Your turn: write a normaliser for messy input
- Write
normalise_username(raw)that strips surrounding whitespace, lowercases the text, replaces spaces with underscores, and removes a leading@. Test it with' @Ada Lovelace '. - Handle repeated underscores by collapsing them, and decide whether a name should end with an underscore. Write your rule down as a comment.
- Add a validation step that rejects anything containing a character other than letters, digits and underscores, and prints a clear message instead of raising an exception.
- Write a second helper,
normalise_title(text), that produces"Ada Lovelace"from"ada lovelace"without usingtitle(), so that"don't"survives intact. - Finally, build a report that reads a list of raw names and prints each one alongside its normalised form and a yes-or-no validation result.
Method cheat sheet
strip(),lstrip()andrstrip()remove whitespace, or a set of characters when you pass one.split()with no argument splits on whitespace and drops empty pieces; with a separator it splits literally and keeps them.- Use
casefold()for comparisons, and avoidtitle()on text with apostrophes. startswith()andendswith()accept tuples;removeprefix()andremovesuffix()strip a known edge.partition()splits into head, separator and tail at the first match.- The
is*predicates returnFalsefor empty strings, andisdigit()accepts characters thatint()rejects.