This is one of those small exercises that looks trivial at first glance but is actually a great way to practice several core Python skills at once: taking user input in a loop, working with strings, and applying conditional filtering. I remember being assigned almost this exact problem early on, and I still use variations of this pattern today whenever I’m filtering log lines or text data by a prefix. In this guide, I’ll build the solution step by step, explain exactly how string prefix-checking works internally, and cover a few different ways to solve the same problem.
Understanding the Problem
I need to:
- Read a series of strings from the user (I’ll decide how many, or let them signal when they’re done).
- Check each string to see if it starts with the letters
"Th". - Print only the strings that match that condition.
The core challenge is deciding how to check whether a string “begins with” a specific substring, and how to read a variable number of strings interactively.
Solution 1: Reading a Fixed Number of Strings
The simplest version asks the user how many strings they want to enter, then loops that many times:
def find_th_strings():
count = int(input("How many strings will you enter? "))
matches = []
for i in range(count):
text = input(f"Enter string {i + 1}: ")
if text.startswith("Th"):
matches.append(text)
print("\nStrings starting with 'Th':")
if matches:
for match in matches:
print(match)
else:
print("No matching strings found.")
find_th_strings()
Example run:
How many strings will you enter? 5
Enter string 1: Thunder
Enter string 2: Apple
Enter string 3: There
Enter string 4: Banana
Enter string 5: Thin
Strings starting with 'Th':
Thunder
There
Thin
The key line here is text.startswith("Th"). This is Python’s built-in string method specifically designed for exactly this kind of check, and I’ll explain how it behaves internally shortly.
Solution 2: Reading Strings Until the User Says “Done”
In real interactive scripts, I usually don’t want to ask “how many” up front — I prefer letting the user type until they’re finished:
def find_th_strings_dynamic():
matches = []
print("Enter strings one at a time. Type 'done' to finish.")
while True:
text = input("String: ")
if text.lower() == "done":
break
if text.startswith("Th"):
matches.append(text)
print("\nStrings starting with 'Th':")
for match in matches:
print(match)
find_th_strings_dynamic()
Example run:
Enter strings one at a time. Type 'done' to finish.
String: Theatre
String: Orange
String: Thankful
String: Grape
String: done
Strings starting with 'Th':
Theatre
Thankful
I actually prefer this version for real-world use, because it doesn’t force the user to know the count in advance — a small usability detail that matters more than it seems.
Handling Case Sensitivity
By default, .startswith("Th") is case-sensitive — it will match "Thunder" and "There", but not "thunder" (lowercase ‘t’) or "THIN". Depending on the requirement, I sometimes want a case-insensitive match instead:
def find_th_strings_case_insensitive():
matches = []
count = int(input("How many strings? "))
for _ in range(count):
text = input("Enter a string: ")
if text[:2].lower() == "th":
matches.append(text)
print("\nMatches (case-insensitive):")
for match in matches:
print(match)
find_th_strings_case_insensitive()
Example run:
How many strings? 3
Enter a string: theory
Enter a string: THIN
Enter a string: Water
Matches (case-insensitive):
theory
THIN
Here I used slicing (text[:2]) combined with .lower() instead of .startswith(), because I wanted to normalize case before comparing. I could also write text.lower().startswith("th"), which achieves the same result and is arguably more readable.
Solution 3: A One-Line List Comprehension Version
Once I collect all the strings into a list, I can filter them concisely using a list comprehension — this is the more “Pythonic” approach I gravitate toward once a script matures past the beginner stage:
strings = ["Thunder", "Apple", "There", "Banana", "Thin", "Grape"]
matches = [s for s in strings if s.startswith("Th")]
print(matches)
# Output: ['Thunder', 'There', 'Thin']
This single line does exactly what the multi-line loop version does, just more compactly. I use this pattern constantly once I already have data in a list, rather than reading it interactively.
How .startswith() Works Internally
str.startswith(prefix) checks whether the string begins with the exact sequence of characters given, comparing character by character from the start of the string. It short-circuits — meaning it stops comparing as soon as it finds a mismatch, so checking a long string against a short prefix like "Th" is very fast: the time complexity is O(k), where k is the length of the prefix being checked, not the length of the whole string.
This is more efficient and more explicit than the alternative of manually slicing the string (text[:2] == "Th"), because .startswith() also gracefully handles edge cases — for example, if the string is shorter than the prefix, .startswith() simply returns False instead of raising an error or behaving unexpectedly:
print("T".startswith("Th")) # Output: False (string too short, no error)
print("".startswith("Th")) # Output: False (empty string)
.startswith() also accepts a tuple of prefixes, which is genuinely useful if I ever need to match more than one starting sequence at once:
words = ["Thunder", "Apple", "There", "Charlie", "Thin"]
matches = [w for w in words if w.startswith(("Th", "Ch"))]
print(matches)
# Output: ['Thunder', 'There', 'Thin', 'Charlie']
Solution 4: Using Regular Expressions
For more complex matching rules — for example, if I wanted to match strings starting with “Th” followed only by letters, and reject anything with leading digits or symbols — I’d reach for the re module instead of a plain .startswith() check:
import re
strings = ["Thunder", "Th3atre", "There", "123Thin", "Thin"]
pattern = re.compile(r"^Th[a-zA-Z]*$")
matches = [s for s in strings if pattern.match(s)]
print(matches)
# Output: ['Thunder', 'There', 'Thin']
Here, ^Th[a-zA-Z]*$ means: the string must start (^) with Th, followed by zero or more letters, and end ($) there — no digits, no symbols. "Th3atre" and "123Thin" are correctly excluded because they don’t match this stricter pattern. I only reach for re when the matching logic genuinely needs this kind of precision; for a plain prefix check, .startswith() remains simpler, faster, and more readable.
Handling Edge Cases Properly
A few edge cases are worth testing explicitly, because they’re easy to overlook:
test_strings = ["", "Th", "T", "th", " Thunder", "THUNDER"]
for s in test_strings:
result = s.startswith("Th")
print(f"{s!r}: {result}")
Output:
'': False
'Th': True
'T': False
'th': False
' Thunder': False
'THUNDER': False
Notice "Th" on its own matches (it is exactly the prefix), "T" alone doesn’t (too short to contain the full prefix), and " Thunder" with a leading space doesn’t match either, because the space itself is the first character. This last case is exactly why I now call .strip() on interactive input before running any prefix check — trailing or leading whitespace from copy-pasted input is a surprisingly common source of “why isn’t this matching?” bugs.
text = input("Enter a string: ").strip()
if text.startswith("Th"):
print("Match!")
Sorting the Matches Alphabetically
A small but genuinely useful enhancement to the original exercise: once I’ve collected the matches, sorting them makes the output easier to read, especially with a longer list:
strings = ["Thunder", "Apple", "There", "Banana", "Thin", "Grape", "Theatre"]
matches = sorted(s for s in strings if s.startswith("Th"))
print(matches)
# Output: ['Theatre', 'Thin', 'Thunder']
Combining filtering and sorting into a single expression like this is a habit I picked up once I got comfortable chaining Python’s built-in functions together instead of writing everything as separate steps.
Common Mistakes I’ve Made
- Forgetting case sensitivity — expecting
"thin"to match"Th"by default, when.startswith()is strictly case-sensitive unless I normalize the case myself. - Using
ininstead of.startswith()—"Th" in textchecks whether"Th"appears anywhere in the string, not specifically at the beginning, which gives wrong results for something like"Weather". - Off-by-one slicing errors when trying to manually check the first two characters, especially forgetting to guard against strings shorter than 2 characters, which
.startswith()handles gracefully but manual slicing comparisons can mishandle. - Not trimming whitespace — input like
" Thunder"(with a leading space) won’t match.startswith("Th"), so I now often call.strip()on user input before checking.
Real-World Applications
This exact pattern — filtering a list of strings by prefix — comes up constantly in real scripting work: filtering log file lines that start with a specific error code, filtering filenames that begin with a certain naming convention before a batch rename, or filtering database column names that follow a particular prefix convention. The core technique (.startswith() combined with a loop or list comprehension) scales directly from this small exercise to real production text-processing scripts.
Frequently Asked Questions
Does .startswith() work with multiple prefixes at once? Yes — pass a tuple of prefixes, like text.startswith(("Th", "Ch")), and it returns True if any of them match.
Is .startswith() faster than slicing and comparing manually? It’s comparable in raw speed for short prefixes, but .startswith() is safer and clearer because it handles short strings and edge cases without raising errors.
How do I make the check case-insensitive? Convert both the string and the prefix to the same case first, typically with .lower(), before calling .startswith().
Can I check if a string ends with a certain substring the same way? Yes — Python provides the mirror-image method str.endswith() for exactly that purpose.
Summary
This exercise is a small but genuinely useful demonstration of reading interactive input, applying string prefix checks with .startswith(), and filtering collections either through explicit loops or Pythonic list comprehensions. The underlying technique — matching strings by their beginning characters — is one I still use regularly in real automation and text-processing scripts.