Regex anchors: ^, $ and multiline mode explained
^ and $ don't match characters, they match positions. Why a pattern without anchors checks less than you think, what the m flag changes, how Python's $ treats a trailing newline, and when to use \A and \z instead.

· 6 min read
/^\d{5}$/
match
- 90210
- 10001
- 02139
- 60614
spare
- 9021
- 902100
- ZIP 90210
- 90210-1234
Most regex tokens match a character. Anchors match a position: the start of the text, the end, the gap between two characters. They take up no width at all.
They're also the most common thing missing from a validation pattern. This post covers ^ and $, what the m flag changes, and the corner cases that differ between languages. Every example is live, so edit the patterns and watch the lines change.
01A pattern without anchors checks less than you think
Regex engines search: they report a match if the pattern fits anywhere in the text. So a "five digit ZIP code" pattern without anchors is really "contains five digits in a row":
02^ and $
^ matches at the start of the text and $ at the end. Put them around the pattern and the whole input has to fit:
/^\d{5}$/Should match2/2
- 90210
- 02139
Should not4/4
- 9021
- 902100
- ZIP 90210
- 90210-1234
You can use just one of them. ^# means "starts with #", and \.pdf$ means "ends with .pdf":
/\.pdf$/Should match2/2
- report.pdf
- 2026 taxes.pdf
Should not2/2
- report.pdf.exe
- pdf-guide.txt
03Anchors around alternation
| has the lowest priority of anything in a pattern, lower than the anchors. So ^cat|dog$ doesn't mean "exactly cat or dog". It means "starts with cat, or ends with dog":
Wrap the choices in a group so both anchors apply to both:
This one bites in real code, in allow-lists like ^admin|editor$ that quietly accept admin-old or not-an-editor.
04Multiline mode
By default, ^ and $ match only at the very start and end of the whole text, even if it contains line breaks. The m flag changes them to match at the start and end of every line:
const text = "apple\nbanana\ncherry";
text.match(/^\w+$/g); // null: "apple\nbanana\ncherry" isn't one word
text.match(/^\w+$/gm); // ["apple", "banana", "cherry"]
That's what you want when you're pulling things out of a file line by line. In Python it's re.MULTILINE, or (?m) at the start of the pattern:
import re
re.findall(r"^#+ .*$", markdown, re.MULTILINE) # every heading line
The flag only changes ^ and $. A dot still won't cross a line break. That's a different flag, s, covered in the post on regex flags.
05The trailing newline trap
Here the languages split. In JavaScript, without m, $ matches only at the very end. In Python, $ also matches just before a newline at the very end:
import re
re.match(r"^\d{5}$", "90210\n") # matches in Python
re.fullmatch(r"\d{5}", "90210\n") # None
/^\d{5}$/.test("90210\n"); // false in JavaScript
So in Python, ^\d{5}$ accepts a ZIP code with a newline stuck on the end. That can matter if the value goes into a database or a file name. Two fixes:
- Use
re.fullmatch(), which must consume the whole string. - Use
\Aand\Z: in Python,\Ais the very start and\Zthe very end, with no newline exception. (Ruby, PCRE and Java spell the strict end\z.)
JavaScript has no \A or \z, but without the m flag its ^ and $ already behave like them.
06Word boundaries are anchors too
\b is another zero-width anchor: the position between a word character and a non-word character. It's how you match a whole word inside text:
07Try one
Catch the lines that are a hashtag and nothing else:
Catch hashtags: a # followed by letters, digits or underscores, and nothing else.
Targets · 0/5 caught
- #regextarget
- #100DaysOfCodetarget
- #atarget
- #snake_casetarget
- #2026target
Innocents · 0 hit
- regexinnocent
- ##doubleinnocent
- #two wordsinnocent
- #innocent
- #dash-edinnocent
- a#taginnocent
Type a regex — the board updates as you go.
08Checklist
- Without anchors, a pattern only checks that the text contains a match.
^is the start and$is the end. Use both for validation.- Group alternatives inside anchors:
^(cat|dog)$, not^cat|dog$. - The
mflag makes^and$work per line. - In Python,
$allows one trailing newline. Usere.fullmatch()or\Z. \banchors a whole word.
09Keep practising
- Anchor's away and Line wrangling are practice packs built around anchors.
- The guide's anchors section has the full list.