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:

textOutput
re.error: bad escape \p at position 2

JavaScript (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:

bashShell
pip install regex
pythonPython
import 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 wantedstdlib re approximationCaveat
[\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:

pythonPython
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:

pythonPython
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} supportNotes
Python rebad escape \p — this page
Python regexfull property + script support
JavaScript✅ with /u or /v flagwithout 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

All guidesOpen the RegexPro tester →