Intuition The 80/20 of this note
A regular expression (regex) is a tiny pattern language for describing the shape of text — not the exact text. You write a pattern once, and the regex engine scans any string to find every place that matches that shape. Master these 5 verbs and you have ~80% of real-world text work: search, match, findall, groups, sub.
Suppose you must pull every phone number, email, or date out of a messy log file. Writing if/for/.split() chains by hand is brittle: there are too many variations ("12-05-2024", "12/05/24", "Dec 5"). A regex lets you describe the family of shapes in one compact string. The engine does the looping and backtracking for you.
The mental model:
You give a pattern (a description of a shape).
The engine moves a cursor left→right and asks "does the text starting here match my shape?"
It reports matches, optionally pulling out groups (sub-pieces you captured).
Definition Core pattern atoms
==.== matches any single character (except newline).
==\d== a digit [0-9]; \w a word char [A-Za-z0-9_]; \s whitespace.
==[abc]== a character class — one char from the set; [^abc] = not those.
Anchors : ==^ start of string, $== end of string.
Quantifiers : ==* 0+, + 1+, ?== 0 or 1, {m,n} between m and n.
==( ... )== a capturing group — remembers what it matched.
(?: ... ) a non-capturing group (group for structure, don't remember).
| alternation ("or"); \b a word boundary.
* feels like "any text" but isn't
Beginners read * as "anything here" (like shell globbing where *.txt). The fix: in regex * is a quantifier — it means "repeat the previous atom 0+ times". So a* = zero-or-more as. To mean "any run of characters" you need .* (any char, repeated).
Definition The five verbs
==re.search(pat, s)== finds the first match anywhere , returns a Match object (or None).
==re.match(pat, s)== only matches at the start of s.
==re.findall(pat, s)== returns a list of all matches as strings (or tuples if groups exist).
==re.sub(pat, repl, s)== replaces every match with repl, returns a new string.
re.finditer(pat, s) like findall but yields Match objects (gives positions).
match vs search
re.match("dog", "the dog") returns None ! Why it feels wrong: "the dog clearly contains dog." The fix: match is anchored at position 0 only. Use ==search== to look anywhere.
Python strings treat \ as an escape (\n = newline). Regex also uses \ (\d, \b). Two layers fighting! Write patterns as raw strings r"\d+" so Python passes the backslash through unchanged to the regex engine.
"\d" vs r"\d"
Feels fine because "\d" often works (Python leaves unknown escapes alone, for now — it's deprecated). The fix: always use r"..." for patterns so \b, \d, \w behave reliably.
Worked example 1 — Extract all integers
import re
re.findall( r " \d + " , "order 12 of 345 items" ) # -> ['12', '345']
Why \d+? \d = a digit, + = "one or more in a row", so consecutive digits clump into '12' and '345' instead of '1','2',.... Without + you'd get every single digit separately.
Worked example 2 — Groups split a date
m = re.search( r " (\d {2} ) / (\d {2} ) / (\d {4} ) " , "born 05/12/2024 ok" )
m.group( 0 ) # '05/12/2024' (whole match)
m.group( 1 ) # '05' (first group)
m.groups() # ('05', '12', '2024')
Why parentheses? Each ( ) captures a sub-piece so you can pull day/month/year out separately. group(0) is always the full match; group(n) the n-th captured group.
findall returns tuples when there are groups
re.findall( r " (\w + ) @ (\w + ) " , "a@x b@y" ) # -> [('a','x'), ('b','y')]
Why tuples? When the pattern has 2+ groups , findall returns each match as a tuple of its groups (not the whole match). With 0 groups it returns whole matches; with 1 group it returns just that group's text.
sub with a backreference
re.sub( r " (\w + ) @ (\w + ) " , r " \2 . \1 " , "a@x" ) # -> 'x.a'
Why \1/\2? In the replacement, \1 means "text captured by group 1". So we swap the two halves. This is how you reformat matched text.
Worked example 5 — Greedy vs lazy
re.findall( r " < . * > " , "<a><b>" ) # -> ['<a><b>'] greedy: grabs as much as possible
re.findall( r " < . *? > " , "<a><b>" ) # -> ['<a>', '<b>'] lazy: as little as possible
Why? * is greedy — it matches as far right as it can, then backtracks. Adding ? after a quantifier makes it lazy (minimal match). Crucial for parsing tags/quotes.
If you use one pattern many times, ==re.compile== once into a pattern object: p = re.compile(r"\d+"), then p.findall(s). Same behaviour, but the pattern is parsed only once — faster in loops and reads cleaner.
Recall Feynman: explain to a 12-year-old
Imagine you have a giant book and you want to highlight every phone number. Instead of reading every line yourself, you give a robot a rule card : "find a chunk that looks like some digits, a dash, some more digits ." The robot zooms through and highlights every chunk that fits the card. Regex is that rule card. ( ) is you drawing little boxes on the card saying "and remember what's inside this box separately." findall = "show me every highlight." sub = "replace every highlight with this new text."
"SMFS-G" = S earch (anywhere), M atch (start only), F indall (list), S ub (replace), G roups (the boxes).
For quantifiers: "Plus = Present (1+), Star = Sometimes (0+), Question = Quitable (0/1)."
What does re.search return when nothing matches? None (so you must check before calling .group).
Difference between re.match and re.search? match only checks the start of the string; search scans anywhere .
What does \d+ mean? One or more digits in a row.
Why use raw strings r"..." for patterns? So Python doesn't eat the backslash; the regex engine gets \d, \b etc. intact.
What does findall return when the pattern has 2 capturing groups? A list of tuples , one tuple of group strings per match.
What is group(0)? The entire matched substring.
How do you make a quantifier lazy? Add ? after it (e.g. .*?), so it matches as few chars as possible.
In re.sub, what does \1 mean in the replacement string? The text captured by group 1 (a backreference).
What is a non-capturing group and why use it? (?:...) groups for structure/alternation without storing it as a numbered group.
What do ^ and $ anchor? ^ = start of string, $ = end of string.
Why is re.match("dog","the dog") None? match is anchored at position 0; "dog" isn't at the start.
What does re.compile buy you? Parses the pattern once into a reusable object — faster in loops.
String methods — split, join, format
Python Intermediate — File I/O and parsing logs
Finite State Machines (regex engines are NFAs/DFAs under the hood)
Greedy vs Backtracking algorithms
Escape characters and raw strings
findall returns tuples via
Intuition Hinglish mein samjho
Regex matlab ek chhoti si "pattern language" jisse hum text ka shape describe karte hain, exact text nahi. Jaise tum robot ko ek rule-card do: "kuch digits, phir slash, phir aur digits" — aur robot poore log file me se saare dates highlight kar deta hai. Isse tumhe for loops aur split ki gandi chain likhne ki zarurat nahi padti. Yaad rakhne wale 5 verbs: search (kahin bhi pehla match), match (sirf start me), findall (saare matches ki list), sub (sabko replace karo), aur groups (jo ( ) ke andar capture hua).
Sabse important atoms: \d = digit, \w = word char, . = koi bhi ek char, aur quantifiers * (0 ya zyada), + (1 ya zyada), ? (0 ya 1). Ek bada confusion: * ka matlab "anything" nahi hota — wo pichhle atom ko repeat karta hai. "Any text" ke liye .* chahiye. Aur greedy vs lazy bhi yaad rakho: .* zyada se zyada match karega, .*? kam se kam — HTML tags parse karte time ye bachata hai.
Groups (( )) ka kaam hai matched text ke tukde alag se nikalna. Jaise date 05/12/2024 me teen groups: day, month, year. group(0) hamesha poora match deta hai, group(1) pehla box. Aur sub me \1, \2 se tum captured tukde reuse karke text ko reformat kar sakte ho. Ek aakhri tip: pattern hamesha raw string r"\d+" me likho, taaki Python backslash ko kha na jaye.