How to Use Unicode Properties in Regex Step by Step
Match letters, digits and scripts in any language with regex Unicode properties: support in grep -P, JavaScript, PHP and Python, flags and gotchas.

The pattern [a-zA-Z]+ was fine for English and fails on a name like "Renée" or "Zoë", or on a username in Cyrillic or Chinese. Writing every range by hand is not viable. Unicode properties describe characters by what they are, and every serious regex engine supports them, with syntax variations. They relate to POSIX character classes and the encoding notes in encoding and locale debugging. The examples assume Node.js 22, PHP 8.3 and GNU grep 3.11.
The common properties
\p{L} any letter \p{Lu} uppercase letter \p{Ll} lowercase letter
\p{N} any number \p{Nd} decimal digit \p{P} punctuation
\p{Z} separators/spaces \p{S} symbols \p{Emoji} emoji (some engines)
\p{Script=Greek} Greek script \p{Han} Chinese characters \P{L} not a letter
The categories come from the Unicode Character Database: a one-letter general category (L, N, P) and two-letter subcategories (Lu, Nd). Script and block names vary slightly between engines. Check which spelling yours accepts.
JavaScript
const word = /^[\p{L}\p{M}'-]+$/u;
console.log(word.test("Renée"), word.test("Zoë"), word.test("Иван"), word.test("12"));
console.log("Ünï 123".match(/\p{Lu}/gu));
console.log("hello wörld".replace(/\P{L}/gu, ""));true true true false
[ 'Ü' ]
hellowörld
Without the u flag, \p is treated as an escaped letter p, not a property, which fails quietly. \p{M} covers combining marks, needed when the text is in decomposed form (an "e" followed by a combining accent). JavaScript's \w stays ASCII-only even with u, so use [\p{L}\p{N}_] for a Unicode word character. More in JavaScript regex.
PHP, grep and Python
php -r 'var_dump(preg_match("/^\p{L}+$/u", "Renée"));'
printf 'Renée\nbob\n12\n' | LC_ALL=C.UTF-8 grep -P '^\p{L}+$'
python3 -c 'import regex; print(regex.findall(r"\p{Lu}", "Ünï Ab"))'int(1)
Renée
bob
['Ü', 'A']
PHP needs the u modifier, as covered in PHP preg functions. GNU grep needs -P and a UTF-8 locale, or it treats bytes as characters and may reject the pattern. Python's standard re has no \p; install the regex package for it, or use [^\W\d_] as a Unicode-aware "letter" in re. macOS grep lacks -P; use perl -CS -ne instead.
Characters that are not one code point
perl -CS -e 'print "\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F467} e\x{301}\n"' | perl -CS -ne 'print scalar(() = /\X/g), "\n"'4
A family emoji is several code points joined by an invisible joiner; "e" plus a combining accent is two. PCRE and Perl's \X matches a whole grapheme cluster, which is the unit a reader sees as one character. JavaScript has Intl.Segmenter for the same purpose. Counting or truncating by code point cuts these in half, which is a bug in names, tweets and filenames.
Properties versus ranges
Character classes such as [a-z] match a fixed set of code points and fail on everything outside it. A pattern for a name written as letters and hyphens rejects "Zoë", "Łukasz" and every name in a non-Latin script. Unicode property escapes solve this by matching characters according to the properties the Unicode standard assigns to them: \p{L} for any letter, \p{Lu} for uppercase letters, \p{N} for numbers, \p{P} for punctuation. The pattern describes what the character is, not which code points it occupies, and it stays correct when Unicode adds new letters. The negated form \P{L} matches anything that is not a letter.
Scripts are available too. \p{Script=Greek} or \p{Han}, depending on the engine, selects characters by writing system, which is useful for detecting the language of a field or rejecting mixed scripts in usernames.
Case, normalisation and equivalence
Properties solve classification but not equivalence. The letter é can be one code point or an e followed by a combining acute accent, and the two look identical and compare unequal. Normalise text to a single form, usually NFC, before matching or comparing, using the language's normalisation function. Case-insensitive matching has its own pitfalls: the Turkish dotless letter and the German sharp s do not fold the way English rules suggest, and engines differ in whether they use simple or full case folding. For identifiers and security decisions, normalise and fold first, then compare, and treat visually confusable characters as a separate issue handled by a confusables check.
When validating names, avoid rejecting valid input. A permissive pattern that allows letters, marks, spaces, apostrophes and hyphens, together with a length limit, serves better than a strict one that insults users.
Grapheme clusters and what users see
A user-perceived character, a grapheme cluster, can consist of several code points: a base letter with accents, an emoji with a skin tone or a family emoji built from several people joined by zero-width joiners. The dot in a regular expression matches one code point in most engines, so it splits clusters. The escape \X in PCRE and Perl matches a whole cluster. JavaScript lacks it, but Intl.Segmenter provides the segmentation. When truncating text for display or counting characters for a limit, work on clusters, or the result may cut an emoji in half and print a broken symbol.
Use the u flag in JavaScript and the u modifier in PHP so the engine treats the subject as code points instead of bytes or UTF-16 units. Without it, characters outside the basic plane count as two units.
Portability across tools
Support for property escapes varies. PCRE, Perl, Java, .NET and modern JavaScript support them, with small differences in the property names. Python's built-in re does not support \p, while the third-party regex package does. GNU grep supports them with -P, and plain POSIX tools use bracket classes like [[:alpha:]], whose meaning depends on the locale. Test patterns on the target engine with sample strings in several scripts, including combining marks and an emoji. Keep a small table of inputs and expected results with the code, and rerun it when upgrading the language or library, since Unicode versions advance and a property's membership grows with each release.
One last practical point concerns performance. Property escapes can be slower than simple ranges on some engines because the property tables are large, though modern engines compile them into efficient automata. Measure on realistic input if the pattern runs inside a hot loop. For simple ASCII-only fields, such as hexadecimal identifiers, a plain range remains clearer and faster, and it documents that only ASCII is accepted. Use properties where the input is human language, and ranges where the format is a technical specification.
Document the intent of each property in a comment, since \p{Lu} is cryptic to someone who has not met the notation. A table in the project notes mapping common needs to properties, such as letters, digits, punctuation, spaces and symbols, saves repeated lookups. Keep the Unicode version in mind when behaviour matters across years: new characters are added in each release, and a validation rule that allowed only letters known in an old version rejects names that are legitimate today.
Where this does not apply
If input is restricted by a protocol to ASCII (hostnames, identifiers), an ASCII class is correct and stricter. For validating names, the safest rule is to accept almost anything and normalise, since real names defy patterns. Normalise with NFC before matching to avoid composed versus decomposed mismatches. Property support depends on the Unicode version in the engine, so new emoji may be missing in old runtimes. See the Unicode regular expressions report and the regular-expressions.info Unicode guide.


