TechEarl

Regex Word Boundaries: \b, \B and \w Explained

Regex word boundaries (\b and \B) match positions between word and non-word characters with zero width. The full reference with engine differences, Unicode handling, lookaround alternatives, and worked examples for whole-word replace, search highlighting, and log parsing.

Ishan Karunaratne⏱️ 12 min readUpdated
Share thisCopied
Complete reference for regex word boundaries: \b and \B zero-width assertions, engine-by-engine support (JS, Python, Java, PCRE, POSIX), Unicode handling, and lookaround alternatives. Worked examples for whole-word replace and search highlighting.

\b is the zero-width assertion that matches the position between a word character and a non-word character: the start of "cat" in "the cat" or the end of "cat" in "cat food", but not anywhere inside "scatter". \B is its negation, a position where there is no word boundary. Both are zero-width: they consume no characters, they only assert position.

What is a word boundary (\b) in regex?

A word boundary is the zero-width position between a word character (\w: a letter, digit, or underscore) and a non-word character (\W: anything else), or at a string edge adjacent to a word character. The \b metacharacter matches that position. It is a position, not a character: \b does not match a literal b, and it consumes nothing, so it never "uses up" any of the input.

In one line: \b says "a word starts or ends right here". The classic use is "match a whole word, not the substring inside another word": \bcat\b matches "cat" in "the cat sat" but not "cat" in "scatter" or "category". \B is the negation, every position that is not a word boundary.

That covers the short answer. The rest of this page is the full reference: the exact positions that match, per-engine support (JavaScript, Python, Java, PCRE, .NET, POSIX, Go, Rust), Unicode caveats, the lookaround alternative when you need finer control, and worked examples in Python and JavaScript. The same idea extends to log parsing (token-precise matches), refactor tools (rename only whole identifiers), and search-and-replace (don't mangle "Java" into "Python" inside "JavaScript").

Jump to:

\b is a boundary, not the letter b

This trips up a lot of people searching for "b in regex" or "regex b", so it is worth being explicit. The escape \b (backslash-b) is a word boundary, a zero-width position. It is not the literal character b. To match an actual lowercase b you just write b with no backslash. So:

  • b matches the letter b.
  • \b matches a position (a word boundary), and matches no character at all.
  • \bb\b matches a standalone single-letter word "b" (the boundary, then the letter, then the boundary).

There is one more wrinkle that catches people out: inside a character class, \b changes meaning. In most engines [\b] is the backspace control character (U+0008), not a word boundary. A word boundary is meaningless inside a [...] class, so the engine reuses the escape for backspace there, the same way it does in string literals. If you meant "word boundary", keep \b outside the brackets.

python
import re
re.findall(r"\bb\b", "a b c bb")   # ['b']  - the standalone "b", not the "bb"
re.search(r"[\b]", "a\bc")         # match  - [\b] is backspace inside a class

What does \w mean in regex?

\w matches a single word character: a letter, a digit, or an underscore. In JavaScript and Go it is essentially [a-zA-Z0-9_]; Python string patterns and several other engines include Unicode characters by default. See the engine table below. Add a quantifier to match a run of them, so \w+ matches one or more word characters in a row (\w+ on "snake_case 42" matches snake_case and then 42), and \w* matches zero or more. Its negation, \W, matches anything that is not a word character.

For the ASCII definition, the word characters are:

Character classMembers
Lettersa-z, A-Z
Digits0-9
Underscore_

The underscore is a word character: \bsnake\b does not match inside snake_case, but it does match inside snake-case. The longer pattern \bsnake-case\b also matches the complete snake-case string; its outer boundary assertions do not forbid punctuation inside the match.

Anything not in \w is in \W: spaces, tabs, punctuation, hyphens, and every non-ASCII letter or symbol depending on the engine and its Unicode mode.

Whole-word matching: spaces, hyphens, and underscores

For JavaScript /\bcat\b/ and Python re.search(r"\bcat\b", text):

TextMatch?Why
catYesBoth outer edges are boundaries
a cat!YesSpace and punctuation are non-word characters
cat-foodYesHyphen is not a word character
cat_foodNoUnderscore is a word character
cat2NoA digit is a word character
scatterNoThe substring is inside a larger word

Do not add the JavaScript g flag to a regex you repeatedly call with .test() unless you deliberately manage its state.

Where \b matches: the four positions

A word boundary exists at any position where one side is a word character and the other side is not (including string start/end). The four cases:

PositionWhen it matchesExample
Start of string before a word char\bcat against "cat sat"matches at index 0
End of string after a word charcat\b against "the cat"cat starts at index 4; the boundary is at index 7
After non-word, before word\bcat\b against "the cat sat"matches the standalone cat
After word, before non-wordsame as above, the trailing edge of catmatches

What \b does NOT match: positions where both sides are word characters (\bcat\b against "scatter" finds no match) or both sides are non-word characters ("!@#" has no word boundaries).

PatternInputMatchesDoesn't match
\bbook\bthe book!book
\bbook\bnotebookthe substring is inside a word
\bbook\bbookstoretrailing edge is word-to-word
\bdog\bbulldog ranleading edge is word-to-word
\b\d+\broom 42 floor 342, 3
\b[A-Z]+\busing SQL todaySQL

Negated word boundary: \B

\B matches every position that \b does NOT, namely positions where both sides are word characters, or both sides are non-word characters. It's the assertion for "match this substring only when it is inside a word":

PatternInputMatchesWhy
\Bis\Bthis islandno matcheach is touches a word edge
\Boo\Bbookoo inside bookboth sides are word chars
\Bing\Bsinginging (middle)not standalone
ing\Bsinginging not at endend position is word→string-end
\Bedbeddingmatches inside beddingleft side is word char

The classic use: highlight every occurrence of a sequence as a substring but skip the standalone form. \Bing\b matches ing at the end of a word but not the standalone word "ing".

Engine compatibility

Word boundary support varies by engine, especially around Unicode. The differences matter when you're writing patterns that need to work in more than one runtime.

Engine\b and \BUnicode-aware by defaultNotes
JavaScriptyesnou or v alone does not make \w or \b Unicode-wide. Case folding with i adds a few characters; use property escapes for an explicit Unicode token rule.
Python (re)yesdepends on flagIn Python 3, re.UNICODE is the default for str patterns; explicit re.A (re.ASCII) reverts to ASCII-only. For bytes patterns the default is ASCII.
Python (regex package)yesyes by defaultThe third-party regex package is Unicode-aware by default and exposes finer control with the (?V1) version flag.
Java (java.util.regex)yesno by default\w and \b are ASCII unless you set Pattern.UNICODE_CHARACTER_CLASS (or use (?U) inline). The UNICODE_CASE flag is separate and only affects case-insensitive matching.
PCRE / PCRE2 / PHPyesoptional(*UCP) changes word-character properties; UTF mode handles encoding. In PHP, /u enables UTF mode but does not by itself enable UCP. Use /(*UCP)pattern/u when both are required.
.NET (System.Text.RegularExpressions)yesyes by defaultWord characters and boundaries are Unicode-aware out of the box. RegexOptions.ECMAScript restricts both to ASCII.
Ruby (Onigmo)yesengine-specificDo not assume \w and word-boundary Unicode semantics are identical; use explicit classes and test your Ruby version.
Go (regexp, RE2)yesnoGo's regexp is ASCII for \w and \b and has no Unicode-word flag. For Unicode word matching, match candidate tokens with [\p{L}\p{N}_]+ and check surrounding characters in code. Go regexp has no lookarounds.
Rust (regex crate)yesyes when Unicode is enabled (default)The default crate supports Unicode boundaries. (?-u:\b) requests an ASCII boundary; available syntax also depends on compiled features.
POSIX BRE / EREnon/aWord-boundary syntax is an implementation extension. GNU grep supports \b, \<, and \>; grep -w is available in GNU and BSD grep.

For the broader cross-engine reference, see the Regex Cheat Sheet.

Unicode word boundaries

In JavaScript, adding u does not turn \b into a Unicode word segmenter. These results follow from the mostly ASCII definition of \w:

javascript
/\bna\b/.test("naïve");        // true: ï is not a JS word character
/\bna\b/u.test("naïve");       // true: u does not change that
/\bcafé\b/u.test("café break"); // false: é and the space are both non-word
/\bnaïve\b/u.test("naïve");    // true: the outer n/e edges are word edges

To treat letters, combining marks, numbers, and underscore as a token, use an explicit rule in modern JavaScript:

javascript
const cafe = /(?<![\p{L}\p{M}\p{N}_])café(?![\p{L}\p{M}\p{N}_])/u;
cafe.test("café break");  // true
cafe.test("caféteria");   // false

This defines one token policy; it is not language-aware segmentation. For text without space-delimited words, consider Intl.Segmenter. MDN documents JavaScript's boundary definition.

Python 3 string patterns use Unicode word characters by default:

python
import re
bool(re.search(r"\bcafé\b", "café break"))        # True
bool(re.search(r"\bcafé\b", "café break", re.A))  # False

re.A switches to ASCII rules; byte patterns have different defaults. See the Python re reference. In PCRE2/PHP, UTF encoding and Unicode character properties are separate options; PCRE2 documents (*UCP). Perl's \b{wb} is not portable PCRE2 syntax.

Lookaround alternative for finer control

\b is a yes/no assertion. When you need to assert which kind of character comes before or after, for example "preceded by whitespace specifically, not punctuation", use lookarounds instead:

Goal\b patternLookaround alternative
Standalone whole word\bcat\b(?<!\w)cat(?!\w)
Preceded only by space (not punctuation)not possible(?<=\s)cat\b
Followed only by space or end-of-stringcat\b (too broad)`cat(?=\s
Treat underscore as a separator\bid\b does not match _id(?<![A-Za-z0-9])id(?![A-Za-z0-9])

The last case is particularly useful when working with identifiers: \b treats _ as a word character (so \bid\b won't match the id inside user_id). If you specifically want to find id as a standalone token even when adjacent to underscore, use the explicit lookaround.

Python's built-in re requires fixed-width lookbehind. JavaScript supports lookbehind in modern runtimes; Go's regexp and Rust's regex crate do not support it. Check your engine before copying a lookaround alternative.

Word boundaries in Python and JavaScript

These two dominate, so here is the canonical whole-word match in each. The pattern is the same; what differs is how you express it and how Unicode behaves.

In Python, use a raw string (r"...") so the backslash reaches the regex engine instead of being eaten as a string escape. r"\bword\b" is the idiomatic form:

python
import re

re.findall(r"\bword\b", "word wordsmith password word.")  # ['word', 'word']
re.sub(r"\bcat\b", "dog", "cat scatter cat")              # 'dog scatter dog'
bool(re.search(r"\bcat\b", "the cat sat"))                # True

Python 3 is Unicode-aware by default for str patterns, so r"\bnaïve\b" matches naïve with no extra flag. Pass re.A (re.ASCII) if you specifically want ASCII-only boundaries.

In JavaScript, write the literal with a single backslash (/\bword\b/), or double it when building from a string with new RegExp("\\bword\\b"):

javascript
"word wordsmith password word.".match(/\bword\b/g);  // ['word', 'word']
"cat scatter cat".replace(/\bcat\b/g, "dog");        // 'dog scatter dog'
/\bcat\b/.test("the cat sat");                        // true

JavaScript keeps mostly ASCII word-character semantics even with u or v. For non-ASCII token edges, use an explicit Unicode property class or a segmenter. More on that in Unicode word boundaries above.

Common use cases with examples

1. Whole-word find and replace

The most common use of \b. Rename a variable in code without mangling longer identifiers:

bash
# GNU sed (not portable to every sed): rename 'count' to 'total' but skip 'counter', 'counterpart'
sed -E 's/\bcount\b/total/g' file.txt
python
# Python: same
import re
new = re.sub(r"\bcount\b", "total", source)

2. Highlight search terms

javascript
const term = "react";
const re = new RegExp(`\\b${term}\\b`, "giu");
return text.replace(re, m => `<mark>${m}</mark>`);

The \b anchors prevent highlighting "react" inside "reaction" or "reactor".

3. Extract whole numbers from text

python
re.findall(r"\b\d+\b", "room 42 on floor 3 has 1024 widgets")
# ['42', '3', '1024']

4. Match log tokens precisely

python
# Match LEVEL tokens in a log file: INFO, WARN, ERROR
re.findall(r"\b(?:INFO|WARN|ERROR)\b", log_line)

Without \b, the pattern would also match ERROR inside ERROR_COUNT.

5. Validate identifier-like strings

javascript
// Match identifiers that are at least 3 chars, alphanumeric + underscore
const isIdentifier = (s) => /^\w{3,}$/.test(s);

For input-validation patterns specifically, the Regex Anchors article covers ^ and $ which often pair with \b for full-string assertions.

Common mistakes

Mistake 1: assuming \b separates hyphens. \b treats - as a non-word character, so \bword\b matches word in word-art. If you don't want that, use the explicit lookaround (?<![A-Za-z0-9_-])word(?![A-Za-z0-9_-]).

Mistake 2: assuming u fixes JavaScript word boundaries. It enables Unicode syntax and code-point handling, but does not make every Unicode letter a \w character. Test the edges of the particular token, not just its interior.

Mistake 3: assuming GNU extensions are POSIX. Standard BRE/ERE does not define \b. On GNU or BSD grep, grep -w 'word' file.txt is convenient. For a portable explicit rule, use grep -E '(^|[^[:alnum:]_])word([^[:alnum:]_]|$)' file.txt; this selects lines and consumes the separators as part of the match.

Mistake 4: thinking \B is "the opposite end" of \b. \B is "no boundary HERE", not "boundary on the other side". \Bbook\B matches book only when both edges are inside a word, which is rare for the whole word book (it would need to be inside something like bookbookkeeper).

Mistake 5: confusing \b with ^ and $. ^ and $ anchor to the start and end of the line (or string with the appropriate flag); \b anchors to the start or end of a word. Both are zero-width but operate at different scales. See the Regex Anchors guide for the line/string anchors.

Mistake 6: copying a Python Unicode pattern to Go. Go accepts \b but defines it in ASCII terms. Match Unicode token candidates and inspect their neighbours in code; a lookaround does not compile in Go's built-in regexp package.

What to do next

For more advanced word-matching patterns:

For specific real-world patterns that build on word boundaries:

For the one-page reference with every regex shortcut on one page, see the Regex Cheat Sheet.

FAQ

It matches a position between a word character and a non-word character, including a string edge next to a word character. It consumes no text. Wrap a word with two boundaries to avoid matching it inside a longer word.

Yes in the usual word-character definitions. A whole-word pattern for id does not match id in user_id. To treat underscore as a separator, use an explicit boundary class that leaves underscore out.

The first asserts a word boundary; the second asserts that the current position is not a word boundary. Both are zero-width. Python before 3.14 has an additional empty-string exception for the non-boundary assertion.

No. Use Unicode property escapes to define the token characters you need, or Intl.Segmenter for language-aware word segmentation. The Unicode flag alone does not change the usual ASCII-oriented word-character definition.

Yes, when the other side is a word character. A whole-word pattern for cat matches cat in cat-food, but not in cat_food. Include hyphen in a custom token class if you want to treat cat-food as one token.

See also

Word boundaries are one of the most misunderstood zero-width assertions. If you want the concepts properly grounded:

  • Learning Regular Expressions (Ben Forta). The gentlest on-ramp: short, current, and example-driven. A good first book if you are still finding your feet.
  • Regular Expressions Cookbook (Jan Goyvaerts and Steven Levithan, 2nd edition). Problem-then-solution recipes across eight languages (JavaScript, Python, PHP, Java, .NET, Ruby, Perl, VB). The one to keep next to the keyboard.
  • Mastering Regular Expressions (Jeffrey Friedl, 3rd edition). The definitive deep-dive on how regex engines actually work: backtracking, NFA versus DFA, and the optimisation that makes a pattern fast or catastrophic. Dense, and unmatched once you are past the basics.
TagsRegular ExpressionsRegexWord Boundaries\b\BAnchorsZero-Width Assertions

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Systems and Network Architect · Chief Technology Officer

Systems and network architect and Chief Technology Officer with more than two decades designing, building, and running production software, cloud and network architecture, Linux systems, and the bare metal underneath them, and lately working AI into the stack. A US Army veteran who served in Operation Iraqi Freedom. What I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

grep -E vs grep -P explained: basic regex (BRE) treats + ? | ( ) { } as literal text, extended regex (ERE) makes them metacharacters, and PCRE adds lookaround and \d. Plus why macOS BSD grep has no -P.

grep Regex: BRE vs ERE vs PCRE Explained

grep has three regex engines and the default one surprises everyone: in basic regex (BRE) the characters + ? | ( ) { } are literal text until you backslash-escape them. -E switches to extended regex (ERE) where they work bare, and -P unlocks Perl-compatible regex with lookaround and \d. The full BRE vs ERE vs PCRE comparison, the same pattern in all three, and why -P does not exist on macOS.

The PCRE (*ACCEPT) backtracking control verb: how it forces an immediate successful regex match, how capturing groups are closed when it fires, which engines support it, and the backtracking control verb family.

The Regex (*ACCEPT) Control Verb, Explained

What the PCRE (*ACCEPT) backtracking control verb does, how it forces an immediate successful match, how it behaves inside capturing groups, which engines support it, and where it is genuinely useful.