From Python for Beginners, "cat" in text checks whether the exact substring cat appears anywhere in text. The in operator is the right tool whenever you know the precise characters you are looking for.
But "three digits followed by a dash" is a pattern, not a fixed substring, and no amount of in checks expresses it cleanly. Patterns are the job of regular expressions, which is what this unit covers.
Patterns, not substrings
Working engineers meet pattern-shaped problems weekly: validate a product code, pull the order numbers out of ten thousand log lines, find every price in a receipt. String methods can only test exact substrings, so Python ships a small pattern language for describing text by shape instead.
A regular expression (regex) describes a shape of text. Python's re module matches those shapes:
import re m = re.search(r"\d\d\d", "order 472 shipped") m.group() # '472'
The building blocks:
| Piece | Matches |
|---|---|
cat | the literal letters c, a, t |
\d | one digit, \w a letter/digit/_, \s whitespace |
. | any single character |
[aeiou] | one character from the set |
[^0-9] | one character NOT in the set |
Always write patterns as raw strings r"..." so backslashes reach the regex engine untouched. re.search finds the first match anywhere and returns a match object, or None if nothing matched, so if m: is the standard guard.
Quantifiers and anchors
Repetition comes from quantifiers, which apply to the piece right before them:
| Quantifier | Meaning |
|---|---|
+ | one or more |
* | zero or more |
? | zero or one |
{3} | exactly 3 |
{2,4} | between 2 and 4 |
So \d+ is a whole run of digits and [a-z]{3} is exactly three lowercase letters. Anchors pin the pattern in place: ^ means start of string, $ means end. ^\d{4}$ matches a string that is entirely four digits, nothing before or after.
Searching, then anchoring
re.search scans for the first match anywhere in the string and returns a match object, or None when nothing matches. Anchors change that behavior by demanding the pattern cover the whole string.
import re m = re.search(r"\d+", "order 472 shipped, 9 items") if m: print(m.group()) print(bool(re.search(r"^\d{4}$", "2024"))) print(bool(re.search(r"^\d{4}$", "year 2024")))
Output
472
True
FalseThe if m: guard is not optional politeness. A failed search returns None, and calling .group() on None raises AttributeError, so real code always checks first.
The last two lines show the anchors at work. ^ means start of string and $ means end, so ^\d{4}$ accepts 2024 but rejects year 2024. Without the anchors that second check would have succeeded, since the digits are in there somewhere.
Validating a product code
A product code here is exactly two uppercase letters followed by exactly three digits, with nothing else in the string. Three candidates show what the pattern accepts and rejects.
import re candidates = ["AB123", "AB12", "xAB123"] pattern = r"^[A-Z]{2}\d{3}$" for c in candidates: print(c, bool(re.search(pattern, c)))
Output
AB123 True AB12 False xAB123 False
[A-Z]{2} is two characters drawn from the uppercase range, and \d{3} is three digits. Both ends are anchored with ^ and $, which is what turns a search into a validation: any extra character anywhere causes a failure. AB12 fails on the digit count, and xAB123 fails only because of the anchors. Drop the ^ and the pattern would happily find AB123 sitting inside xAB123 and report a match.
Validating a clock time
A 24-hour time like 09:30 is two digits, a literal colon, then two more digits, and nothing else.
import re candidates = ["09:30", "9:30", "09:30pm"] pattern = r"^\d{2}:\d{2}$" for c in candidates: print(c, bool(re.search(pattern, c)))
Output
09:30 True 9:30 False 09:30pm False
\d{2} matches exactly two digits, and the colon is an ordinary literal character with no special meaning in a pattern. 9:30 fails because a single digit is not two, and 09:30pm fails purely on the $ anchor, since the trailing letters are extra characters the pattern does not allow.
This pattern checks shape, not validity.
99:99passes it. Range checking is a separate job, and doing it in a regex gets ugly fast, so convert to numbers and compare instead.
The optional quantifier
The pattern r"ab?c" matches both ac and abc, because the b is optional.
? means zero or one of the piece immediately before it, and here that piece is just the single character b. That last point is the one people trip over: a quantifier attaches only to what directly precedes it, never to the whole pattern. To make a multi-character group optional you have to bracket it, as in r"a(bc)?d".
Compare it with the neighbours from the quantifier table above. r"ab+c" demands at least one b, so it rejects ac. r"ab*c" allows any number of them, so it accepts ac, abc, and abbbc alike.