Unicode Property Escapes (`\p{L}`) in Python: Why `re` Says No
Python's re module has never supported \p{L} Unicode property escapes — four ways around it, from the regex package to stdlib-only rewrites.
The error that brought you here:
re.error: bad escape \p at position 2JavaScript (with the u flag), Go, Java, and PCRE all understand \p{L} — "any Unicode letter." Python's standard-library re module doesn't support Unicode property escapes at all, and never has. It's one of the longest-standing gaps between re and every other modern engine.
Your four options, best first
Option 1 — the regex module (the real fix)
The third-party regex package is a drop-in replacement for re, maintained for years precisely to fill gaps like this:
pip install regeximport regex
# Unicode-aware slug check — letters/digits in ANY script
# (matches RegexPro's slug-unicode pattern)
pattern = regex.compile(r"^[\p{L}\p{N}]+(?:-[\p{L}\p{N}]+)*$")
pattern.match("café-au-lait") # ✓
pattern.match("東京-2026") # ✓
pattern.match("hello world") # ✗ (space)Same API as re (match, search, sub, groups — everything), plus \p{...}, \P{...} (negation), script properties (\p{Script=Han}), possessive quantifiers, and more. If your project can take the dependency, stop reading here — this is the answer.
Option 2 — exploit re's Unicode-aware shorthands
Python's \w, \d, \b are already Unicode-aware by default on str — this surprises people coming from other engines. \w matches é, 東, ß out of the box. So many \p{...} patterns have a stdlib approximation:
| You wanted | stdlib re approximation | Caveat |
|---|---|---|
[\p{L}\p{N}] | \w minus _: [^\W_] | \w includes underscore; the double-negative excludes it |
\p{N} or \p{Nd} | \d | \d matches all Unicode decimal digits (e.g. ٣), which may be more than you wanted |
\p{L} alone | [^\W\d_] | "word char that isn't a digit or underscore" — the classic idiom |
The Unicode slug pattern (^[\p{L}\p{N}]+(?:-[\p{L}\p{N}]+)*$) in stdlib-only Python:
import re
slug_re = re.compile(r"^[^\W_]+(?:-[^\W_]+)*$")Double negatives are ugly but exact: [^\W_] = "not(non-word) and not underscore" = letters and digits, any script.
Option 3 — check properties in code, not in the pattern
str methods and unicodedata speak fluent Unicode:
def is_slug(s: str) -> bool:
return bool(s) and all(
part and all(c.isalnum() for c in part)
for part in s.split("-")
)The general shape: loosen the regex (or drop it), enforce the Unicode property with str.isalpha() / isnumeric() / unicodedata.category(). More verbose, zero dependencies, and often clearer about intent.
Option 4 — spell out the ranges (last resort)
[a-zA-ZÀ-ɏͰ-Ͽ...] — enumerate the blocks you care about. Only defensible when you genuinely mean specific scripts rather than "any letter" (e.g. "Latin plus Latin-extended only" for a legacy system). As an approximation of \p{L} it's a maintenance trap: the Unicode database grows every year and your character class doesn't.
Cross-engine cheat sheet
| Engine | \p{L} support | Notes |
|---|---|---|
Python re | ❌ | bad escape \p — this page |
Python regex | ✅ | full property + script support |
| JavaScript | ✅ with /u or /v flag | without the flag, \p is a literal p (silent!) |
| Go (RE2) | ✅ | native, no flag needed — RE2 is excellent at this |
| Java / PCRE / .NET | ✅ |
Two traps in that table worth naming: JS silently treats \p{L} as the letter p + {L} without the u flag — no error, just wrong matches. And Go, which says no to lookarounds and backreferences, fully supports property escapes — engine capabilities don't line up along a single "power" axis.
Test it live
Run the Unicode slug pattern against JS, Python, and Go simultaneously in RegexPro's tester and watch Python's stdlib reject what the other two accept — then try the [^\W_] rewrite and see it pass everywhere.
Related guides: Lookarounds in Go · Python named groups in JavaScript