What you'll learn
You've learned to make decisions and repeat work. Now apply that control flow directly to text: find a symbol in an address, count vowels, extract part of a ticket ID and inspect fictional log lines.
- Use len(), zero-based indices and negative indices to inspect text safely.
- Slice strings with start, stop and step, including reverse slices.
- Use in and not in for substring membership and case-insensitive checks.
- Distinguish find(), index() and count() when locating text.
- Identify prefixes and suffixes with startswith() and endswith().
- Clean and combine text with strip(), lower(), upper(), replace(), split() and join().
- Write a character-by-character loop that counts matching letters.
- Build a small independent text-processing script and test varied inputs.
A string is a sequence of characters
Python sees text as a sequence, not a mysterious single object. len(text) reports the number of characters, with spaces and punctuation included. Positions begin at zero: name[0] is the first character and name[-1] is the last. Indexing an empty string or an out-of-range position raises IndexError. This is different from slicing, which safely returns shorter strings when a boundary goes beyond the text.
Zero-based indexing
'SW-01'[0] is 'S'; 'SW-01'[1] is 'W'. The index counts characters, not words.
Negative positions
'SW-01'[-1] returns '1', the last character. Before indexing a potentially empty value, check if text: or len(text) > 0.
device = 'SW-01'
print(len(device))
print(device[0])
print(device[-1])What values print, and what would device[5] do? Reveal the explanation after you predict.
The lines print 5, S, and 1. There are five characters with indices 0 through 4; index 5 would raise IndexError.
Every character has a position.
Change the fictional message to see its indexes, slices and searches. Use the real Python editor below to test your understanding.
text[0:5]"SW-01"text[-1]"e"text[::-1]"enilffO ,10-WS"needle in text.lower()truetext.lower().find(needle)7text.lower().count(needle)1Search converts both values to lowercase. This explorer treats an empty search field as no query; Python treats empty substrings differently. A dot (·) represents a space.
Slicing: start, stop and step
A slice uses text[start:stop:step] to build a new string. Start is included, stop is excluded, and the step controls the movement. Omitting start or stop defaults to the relevant end; a negative step can reverse characters. Because strings are immutable, slicing creates a new string and never modifies the original. For beginners, first reason through a simple positive-range slice, then experiment with [-3:] and [::-1].
Stop is exclusive
'INC-2047'[0:3] is 'INC', not 'INC-'. The fourth character at index 3 is excluded.
Reverse with a negative step
'abc'[::-1] produces 'cba'. This is a useful way to practice how the slice step changes direction.
ticket = 'INC-2047'
print(ticket[:3])
print(ticket[-4:])
print(ticket[::-1])What do the first two slices produce, and does the original ticket change? Reveal the explanation after you predict.
The first is INC and the second is 2047. The reversed text is 7402-CNI. The original ticket remains unchanged.
Search with in and not in
Membership testing is one of the most useful beginner programming skills. The expression '@' in address evaluates to True or False; it does not require a loop or complicated regular expression. not in expresses the opposite. Matching is case-sensitive, so normalize with lower() before case-insensitive checks. One symbol is not enough to validate an email address: for now, we are only checking whether a marker exists.
Boolean answer, not a position
'@' in 'student@example.com' returns True. A similar-looking string without @ returns False.
Use lower() for case-insensitive text
'error' in 'ERROR: timeout'.lower() is True. Without lower(), the case mismatch makes the expression False.
email = 'student@example.com'
message = 'ERROR: timeout'
print('@' in email)
print('error' in message.lower())
print('offline' not in message.lower())Why are all three results True? Does @ alone prove the address is deliverable? Reveal the explanation after you predict.
The address contains @, the message becomes lowercase and contains error, and offline is absent. All expressions are True. An @ symbol alone does not establish a valid or deliverable email address.
find(), index() and count() aren't interchangeable
Sometimes you need the location of a substring instead of a Boolean membership result. text.find(part) returns the first start index, or -1 if absent. text.index(part) is similar but raises ValueError when no match exists. text.count(part) counts non-overlapping occurrences. Choose based on whether an absent match is expected or exceptional. A simple membership test is clearer when you only need True or False.
Missing is -1 with find()
'port 443'.find('443') returns 5; finding '22' returns -1. Do not confuse that -1 result with the last character index.
Count repeated markers
'error,error,warning'.count('error') is 2. It counts substring occurrences rather than unique log events.
message = 'error|warning|error'
print(message.find('warning'))
print(message.find('offline'))
print(message.count('error'))Which result means 'not found', and why is that different from index()? Reveal the explanation after you predict.
The results are 6, -1, and 2. find returns -1 when absent, whereas index would raise ValueError. count returns the number of matching substrings.
startswith() and endswith() for predictable patterns
Filenames, ticket identifiers, and log lines often have known beginnings and endings. startswith(prefix) and endswith(suffix) return Booleans. They avoid hand-counting positions for simple prefix/suffix tests. They are case-sensitive and do not validate every detail of a filename; combine with strip() or lower() when the problem requires normalization.
Prefix
'WARN AP-02 offline'.startswith('WARN') is True, useful when separating warnings from info messages.
Suffix
'events.log'.endswith('.log') is True. A filename ending in .log does not prove its contents are trustworthy or actually log data.
filename = '2026-11-08-alert.log'
print(filename.startswith('2026'))
print(filename.endswith('.log'))
print(filename.endswith('.csv'))Why is the final expression False, even though filename contains other periods or dashes? Reveal the explanation after you predict.
endswith checks the actual final characters. The filename ends in .log rather than .csv, so the last result is False.
strip(), lower(), upper(), and replace()
Text from exports or terminals may contain leading spaces, inconsistent capitalization or separators. strip() removes surrounding whitespace; lower() and upper() return transformed versions; replace(old, new) returns a new string with matching substrings replaced. None of these methods changes the original string, because Python strings are immutable. Be deliberate with replacements: replacing every hyphen in a hostname could change its meaning.
Chain small transformations
' Offline '.strip().lower() becomes 'offline'. Read chained methods from left to right.
Immutability
normalized = raw.strip().upper() does not rewrite raw. Store the transformed result in a variable if the caller needs it.
raw = ' ap-02 | OFFLINE '
cleaned = raw.strip().replace(' | ', ': ').upper()
print(cleaned)
print(raw == cleaned)What prints? Does replace alter raw in place? Reveal the explanation after you predict.
The first line is AP-02: OFFLINE and the second is False. The chained methods return new strings; raw still contains its original spacing and capitalization.
split() and join() transform a message
A line such as 'SW-01, offline, HQ' contains several fields separated by commas. split(',') produces a list of parts (including spaces around values), while strip() cleans each one. A delimiter's join(items) creates one string from a sequence of strings. A split is convenient for simple, controlled text formats; for real CSV with quoted commas, use csv.DictReader in the Files lesson.
Split produces a list
'a,b,c'.split(',') returns ['a', 'b', 'c']. You can assign fields by position if you know the exact format and number of fields.
Join expects text
' | '.join(['SW-01', 'offline']) produces 'SW-01 | offline'. To join numbers, convert them to strings first.
raw = 'SW-01, offline, HQ'
parts = [item.strip() for item in raw.split(',')]
host, status, site = parts
print(f'{host} @ {site}: {status}')Why do we strip each field after split? What would happen for a quoted field containing a comma? Reveal the explanation after you predict.
split(',') leaves spaces around fields, so strip cleans them. Splitting a real CSV quoted field with a comma would be incorrect; use the csv module for such files.
Count vowels by visiting characters
A string itself is iterable. You can write for letter in message: to inspect each character in order. Convert the message to lowercase to avoid repeating uppercase comparisons, then use membership letter in 'aeiou'. Keep the counter outside the loop and increment it only for matches. This combines Control Flow with string processing and mirrors the kind of unfamiliar problem a beginner may see in an exam or work task.
Iterate through text directly
You don't need range(len(text)) just to visit each character. for letter in text is simpler and avoids indexing mistakes.
Test zero, one and many matches
Try 'sky' (0 vowels), 'A' (1), and 'Education' (5). The logic should work for each without hardcoding the word.
message = 'Education'
vowel_count = 0
for letter in message.lower():
if letter in 'aeiou':
vowel_count += 1
print(vowel_count)Why does the program print 5, and what would happen if we removed lower()? Reveal the explanation after you predict.
Education contains e, u, a, i, o—five vowels ignoring case. Removing lower() would cause the capital E not to match the lowercase vowel string, giving 4.
Make a small text-based troubleshooting report
Combine the techniques: scan fictional log lines, select those beginning with WARN, extract hostnames and return an alphabetized list of distinct hosts. This exercise introduces a little list handling from the earlier Control Flow lesson, which Working with Data will explore more thoroughly. Do not confuse the presence of a word in text with a validated operational fact. You're writing a local parser for a known fictional format, not examining real devices.
Construct the algorithm
Describe the steps in plain language before writing code: visit every line, choose warnings, extract a hostname, collect distinct hosts, and format a report.
Check changed input
A correct algorithm should behave when a warning repeats, when there are zero warnings, or when a different host appears.
events = ['WARN AP-02 offline', 'INFO SW-01 up']
for event in events:
if event.startswith('WARN '):
parts = event.split()
print(parts[1])What does this print, and why does the other line not appear? Reveal the explanation after you predict.
It prints AP-02. Only the first event begins with WARN, so the INFO line is skipped. A later exercise extends this to multiple unique sorted hostnames.
Inspect a string in real Python
The lesson begins with a guided exercise; complete the full coding clinic afterward. Edit real Python, read the console and trace, then check the result with the given inputs. Change the inputs and test other cases too.
Write it. Run it. Understand it.
Learn character indexing, string searching, slicing and original text algorithms in real Python.
Inspect the first and last characters
Read the supplied device name using input('Device: '), store its first character in first and its last character in last, and print exactly 'SW-01: S / 1'. Do not hardcode the characters; use indexing so changing the hostname changes the result.
Device: SW-01
SW-01: S / 1Responses for input() — only when your script asks
What this does: If your code asks input("Device? ") and then input("Port? "), Python reads your first line as the device and the second as the port. Run uses the responses above. Check uses the exercise's provided test inputs. The result of input() is always text until you convert it. If your code never calls input(), you don't need this panel.
Try solving it before asking for help. Hints never show a completed program.
Your printed output and errors will appear here.
Run the code, then step through the executed lines to watch variable values change.
Move from recognition to independent coding.
Some tasks fix a short script; later tasks require you to write the algorithm yourself. Optional hints provide direction without revealing the finished program.
Continue to all Practical String Processing challenges →After Practice: Working with Data →Official references
Use the official Python documentation to confirm exact syntax or behavior. The browser workbench and the local Python interpreter should agree for these basic operations.
