Find Text Patterns with Regular Expressions
Use regular expressions to find, collect, and check short text patterns without accepting partial matches.
Video lesson: Find Text Patterns with Regular Expressions
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
Imagine a support message that may contain order codes. Our codes have the letters ORD, a hyphen, and exactly four digits. ORD-1204 and ORD-0042 qualify. ORD-12345 has five digits and should be rejected. A short regular expression can find the letters and four digits, but it may still accept the first part of an invalid longer code. That is a subtle bug because the returned value looks perfect when you read it alone. In this lesson we will run a naive pattern, inspect its partial match, and add boundaries so the code must stand on its own. We will also separate finding a code inside prose from validating an entire input string.
- 00:46
You can run the examples in the website console and complete an automatically checked exercise. Before each run, keep the written format rule in view. A pattern is useful only if it matches the rule the program actually needs. A few positive examples are not enough; an almost-correct negative example can reveal the missing condition. The first returned string is not a complete valid code from the message. It is the first four digits of a five-digit value. The expression says where a match starts and what four characters follow; it says nothing about the character after the fourth digit. `findall` searches the whole text and returns every substring that satisfies that narrow instruction.
- 01:30
Python has obeyed the pattern, while the pattern has failed our requirement. This is why regex debugging should include both the matched text and the surrounding characters. If we copied the first result into a report, the invalid number could be silently shortened. Notice that `[0-9]` means an ASCII digit from zero to nine. The shorthand `\d` also matches some non-ASCII digits in a Unicode Python string, which might be appropriate for another format but is wider than our stated code format. The repeated part `{4}` counts exactly four digit characters inside the match. It does not by itself demand that the next character be non-digit.
- 02:13
We need to define the outside edge of the code. The match spans positions zero through seven; the end position eight points to the extra five. This is direct evidence of the failure. We do not need to guess how the engine behaved. `search` looks for one match somewhere in the input and gives us a match object with its text and span. `findall` gave us all matching strings but omitted their positions. Both are useful, depending on whether we need one located match or a list. For a format validator, another API is `fullmatch`, which requires the entire input string to match a pattern. But our support message contains ordinary words around several order codes, so applying fullmatch to the whole message would reject everything.
- 03:00
Here we need to search inside prose and still demand clean boundaries around each candidate. We will use word boundaries. A word boundary is a position between a word character and a non-word character, or the edge of the string. The right boundary rejects a fifth digit or a following letter; the left boundary prevents cutting a code out of a larger word. Python's default Unicode string regex treats Unicode letters as word characters too. The first word boundary keeps the code from starting inside a larger word. The second boundary requires the four digits to finish before another word character begins.
- 03:40
Boundaries check positions without consuming neighboring characters, so the returned strings remain the full order codes. The five-digit candidate is rejected because the next character after four digits is still a word character. The two correct candidates pass. Python's default Unicode matching also treats an accented letter immediately beside the code as a word character. That is useful when our rule says the code must stand on its own in prose. We still use `[0-9]` for the four digits because the code format requires ASCII digits; `\d` would admit additional Unicode digit characters. `re.compile` stores the pattern in a reusable object; you can also call `re.findall` with the same pattern directly for a one-off case.
- 04:29
We use a raw string, marked with `r`, so the backslashes in `\b` reach the regular-expression engine as written. This pattern describes one precise teaching format, not all possible order IDs. If your real product permits other letters, digits, or separators, change the written rule first and then change the pattern and tests together. The tests cover punctuation-separated valid codes, a longer number, a code embedded in a word, Unicode letters touching either side of a candidate, and empty text. Both accented-letter cases are rejected because there is no word boundary between the letter and the code. `fullmatch` accepts a string that is only one complete code and rejects a sentence containing that code.
- 05:16
The same compiled pattern can support both operations because the API controls whether we search inside text or validate the entire string. The website task asks you to return a list of complete codes in input order, so `findall` is the relevant method there. Start with the obvious valid cases, then run the near misses and make sure none are returned. If a test fails, print the exact matched substring and the characters around it; that usually exposes a missing boundary or an overly broad character class. Regular expressions are most maintainable when the input format is short, stated plainly, and backed by examples that almost match.
- 06:00
For a more complicated document format, a parser may be easier to understand. Our small order-code rule is a good place to practice deliberate boundaries.
Understand the concept
String methods are often the simplest way to find or replace fixed text. A regular expression becomes useful when the shape of the text matters: two letters, a dash, and three digits, for example. Python's standard-library re module interprets a pattern. Use a raw string such as r'\b[A-Z]{2}-[0-9]{3}\b' so the boundary backslashes reach the regex parser clearly.
re.search(pattern, text) returns the first match anywhere in the text, or None. re.findall(pattern, text) returns every non-overlapping match. When you need to check that the entire string has the required shape, use re.fullmatch(pattern, text). re.match checks only from the start, so it can still accept extra characters at the end; it is not a substitute for fullmatch.
Boundaries prevent a pattern from accepting part of a longer code. The expression r'\bORD-[0-9]{4}\b' finds ORD-1204 but not the first four digits of ORD-12045. [0-9] intentionally means ASCII digits; \d also matches many other Unicode digits in a Unicode string. Keep the rule narrow and explain which characters are allowed.
The exercise extracts uppercase order codes from a paragraph, preserving their order and duplicates. Try a valid code, an embedded code inside a longer word, one with too many digits, an empty input, and a code at the end of a sentence. For simple fixed text, prefer readable string methods over a complex pattern.
- search finds the first occurrence or None
- findall collects non-overlapping matches
- fullmatch checks the entire string
- Raw strings and boundaries keep patterns precise
See it step by step
Read the code, predict the output, then compare it with the result.
01. Search for one word anywhere
import re
match = re.search(r"cat", "A cat naps.")
print(match.group() if match else "none")catsearch finds cat after the first two characters and returns a match object; the guard handles a possible None.
02. Collect full-shaped codes
import re
text = "AB-123, bad AB-1234, CD-007."
print(re.findall(r"\b[A-Z]{2}-[0-9]{3}\b", text))['AB-123', 'CD-007']The boundaries keep the pattern from returning AB-123 out of the longer AB-1234 code.
03. Require the entire string to fit
import re
pattern = r"ORD-[0-9]{4}"
print(re.fullmatch(pattern, "ORD-1204") is not None)
print(re.fullmatch(pattern, "ORD-12045") is not None)True
Falsefullmatch accepts the four-digit code and rejects a fifth trailing digit.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Search for a pattern within larger text
A regular expression describes a text pattern. re.search scans until it finds a match anywhere in the input, returning a match object or None. In this example, the pattern looks for a hash sign, one uppercase letter, a hyphen, and exactly three digits. The raw-string prefix r keeps the regex backslash syntax readable: \d means a digit and \b marks a word boundary. Without the boundary, a four-digit number could contribute its first three digits as a misleading match.
Always check whether search returned a match before calling group. A missing order ID gives None, and None has no group method. The example tests both a matching message and a message with no order ID. Regular expressions are good for a focused text shape, but the pattern is not a complete definition of all possible order numbers. For more complex formats, write down the accepted and rejected cases before making the pattern larger.
import re
pattern = r"#[A-Z]-\d{3}\b"
for message in ["Order #A-204 is ready", "No order here"]:
match = re.search(pattern, message)
print(match.group() if match else "not found")#A-204
not found- re.search can find the ID inside a longer sentence.
- The pattern requires one uppercase letter and exactly three trailing digits.
- The conditional expression handles a missing match safely.
Validate the whole string with fullmatch
Searching is not validation. re.search can succeed when valid-looking text is followed by an unwanted character, because only some part of the string needs to match. re.fullmatch requires the entire input to fit the pattern. Here a simple stock code must consist of two uppercase ASCII letters, a hyphen, and three ASCII digits. The expression [A-Z]{2} sets the letter count, while [0-9]{3} sets the digit count.
The boolean result makes the rule visible for three inputs: one valid code, one with an extra exclamation mark, and one with lowercase letters. A regular expression validates syntax, not business meaning: it cannot tell whether a syntactically valid stock code exists in a database. Use ordinary parsing or explicit comparisons when they explain a simple rule more clearly. Add AB-20 and AB-020 to the list and predict their results.
import re
pattern = r"[A-Z]{2}-[0-9]{3}"
for code in ["AB-204", "AB-204!", "ab-204"]:
print(code, bool(re.fullmatch(pattern, code)))AB-204 True
AB-204! False
ab-204 False- fullmatch checks the entire input, so the extra ! fails.
- The uppercase range excludes lowercase letters.
- A syntactically valid code still needs a separate existence check if the application requires one.
Extract complete codes without partial matches
re.findall collects every non-overlapping match as a list of strings. The practice function find_codes searches a sentence for complete order codes: the literal prefix ORD- followed by exactly four ASCII digits. Use [0-9] rather than \d when the requirement explicitly says ASCII digits; \d also recognizes digits from other writing systems. Word boundaries, written \b, check the characters just outside the code without including them in the match.
A word boundary before ORD rejects a code joined to another letter on its left. The boundary after the fourth digit rejects a longer code on its right. Python treats Unicode letters as word characters, so the pattern also rejects a code attached to an accented letter. Without these boundaries, a pattern could incorrectly extract ORD-2041 from ORD-20410 or from XORD-2041. findall skips invalid text, so it finds codes in prose rather than validating an entire document. Add a valid code next to punctuation and check that it is still found.
import re
def find_codes(text):
return re.findall(r"\bORD-[0-9]{4}\b", text)
text = "ORD-2041, XORD-9999, ORD-20410, éORD-5555, ORD-1234"
print(find_codes(text))
print(find_codes("nothing to extract"))['ORD-2041', 'ORD-1234']
[]- The two standalone four-digit codes are returned in their source order.
- Unicode-aware word boundaries reject the attached and five-digit impostors.
- When no code matches, findall returns an empty list.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Use re.findall(r'\bORD-[0-9]{4}\b', text). The word boundaries stop the pattern from returning only part of a longer word or number.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Using a non-raw boundary pattern
re.findall("\bORD-[0-9]{4}\b", "ORD-1234")re.findall(r"\bORD-[0-9]{4}\b", "ORD-1234")What happens: The Python string contains backspace characters rather than regex word-boundary escapes, so the expected code is missed.
Prefix a regex pattern with r so backslashes reach the regular-expression parser as intended.
Accepting only the start of an invalid code
re.match(r'ORD-[0-9]{4}', 'ORD-12345') is not Nonere.fullmatch(r'ORD-[0-9]{4}', 'ORD-12345') is not NoneWhat happens: The expression is True even though a fifth digit remains.
Use fullmatch to validate a whole string; use word boundaries when extracting a whole code from a sentence.
Expecting search to return text directly
re.search(r'cat', 'A cat naps.').upper()match = re.search(r'cat', 'A cat naps.')
print(match.group().upper() if match else 'none')What happens: AttributeError because search returns a Match object, not a string.
Check for None, then call group() to get matched text.