Python unicodedata: Normalize Unicode

Published on: August 11, 2026
Reading time: 5 minutes
Three-dimensional alphabet representing Unicode normalization with Python unicodedata

The unicodedata module exposes the Unicode Character Database compiled into the running Python version. Applications can inspect character names, categories, numeric values, bidirectional classes, combining marks, decompositions, and normalization forms. These features are essential for search, imports, identifiers, slugs, comparison, validation, and internationalization.

This guide explains the main APIs and shows how to normalize text without destroying information. Python 3.14.6 documents Unicode Character Database version 16.0.0.

Check the Unicode database version

import unicodedata

print(unicodedata.unidata_version)

The version can affect names, categories, and recognized characters. Systems that persist normalized keys should record their Python version and test upgrades.

Get a character name

name() returns the official Unicode name. Some characters have no assigned name, so provide a default for arbitrary input.

print(unicodedata.name("½"))
print(unicodedata.name("\uFFFF", "UNNAMED"))

Names are useful for diagnostics and reports. They are standardized identifiers, not localized labels for end users.

Look up a character by name

lookup() performs the reverse operation and raises KeyError when the name is unknown.

character = unicodedata.lookup("LEFT CURLY BRACKET")
print(character)

The function also supports aliases and named sequences present in the database. Validate user-provided names and handle failures.

General categories

category() returns a two-letter code. The first letter represents a broad group such as letters, marks, numbers, punctuation, symbols, separators, controls, or unassigned characters.

for character in ["A", "a", "9", "!", " "]:
    print(character, unicodedata.category(character))

Examples include Lu for uppercase letters, Ll for lowercase letters, Nd for decimal digits, and Zs for space separators. Categories alone are not a complete identifier-security policy.

decimal, digit, and numeric values

Unicode distinguishes several numeric concepts. decimal() handles decimal digits, digit() includes other digit forms, and numeric() covers values such as fractions and numerals.

print(unicodedata.decimal("٩"))
print(unicodedata.digit("⁹"))
print(unicodedata.numeric("½"))

Define whether an input field accepts ASCII digits only, international decimal digits, or any Unicode numeric character.

Combining characters

combining() returns the canonical combining class. Zero usually means the character is not a combining mark with a defined class.

text = "a\u0301"
for char in text:
    print(repr(char), unicodedata.name(char), unicodedata.combining(char))

The sequence contains a letter and a separate acute accent. It can look identical to a precomposed á while having different length and bytes.

Why normalization matters

Unicode allows canonically equivalent representations. Without normalization, comparisons, database keys, caches, and search indexes may treat visually identical text as different.

a = "café"
b = "cafe\u0301"
print(a == b)
print(unicodedata.normalize("NFC", a) == unicodedata.normalize("NFC", b))

Normalization does not solve case differences, punctuation, language rules, or visually confusable characters.

NFC and NFD

NFD applies canonical decomposition. NFC decomposes and then recomposes characters when a precomposed form exists.

nfd = unicodedata.normalize("NFD", "action")
nfc = unicodedata.normalize("NFC", nfd)

NFC is a common storage and comparison choice for human text. NFD is useful when analyzing combining marks or building an accent-insensitive auxiliary key.

NFKC and NFKD

Compatibility forms may replace stylistic or historical variants with simpler equivalents.

print(unicodedata.normalize("NFKC", "Ⅳ"))
print(unicodedata.normalize("NFKC", "Full"))

Compatibility normalization can lose meaningful distinctions. Use it for search or identifiers only after defining a domain policy. Do not apply it blindly to passwords, signatures, legal text, or content that must preserve typography.

Check whether text is normalized

is_normalized() tests NFC, NFD, NFKC, or NFKD.

text = "café"
if not unicodedata.is_normalized("NFC", text):
    text = unicodedata.normalize("NFC", text)

Direct normalization is often simple enough, while the check is useful for audits and metrics.

Removing accents carefully

A common pattern decomposes text and removes nonspacing marks. This is not universal transliteration.

def remove_accents(text):
    decomposed = unicodedata.normalize("NFD", text)
    filtered = "".join(
        char for char in decomposed
        if unicodedata.category(char) != "Mn"
    )
    return unicodedata.normalize("NFC", filtered)

Use the result as an auxiliary search key, not as a replacement for original content. Many letters do not reduce correctly with this method.

Character decomposition

decomposition() returns hexadecimal code points and sometimes a compatibility tag.

print(unicodedata.decomposition("Ã"))
print(unicodedata.decomposition("①"))

This is useful for diagnostics. For normal transformation, normalize() is usually the right API.

Bidirectional text

bidirectional() returns a character’s bidirectional class. mirrored() identifies symbols that may be mirrored in right-to-left layout.

print(unicodedata.bidirectional("٧"))
print(unicodedata.mirrored(">"))

These properties do not replace a full layout engine. Bidirectional controls can make code, logs, and file names visually misleading, so security tools should offer escaped views.

East Asian width

east_asian_width() categorizes characters as narrow, wide, fullwidth, halfwidth, ambiguous, or neutral.

for char in "A界F":
    print(char, unicodedata.east_asian_width(char))

The property helps estimate display width, but combining marks, emoji sequences, and terminal policy still matter.

Casefolding and normalization

For caseless matching, normalize and apply casefold().

def search_key(text):
    return unicodedata.normalize("NFKC", text).casefold()

print(search_key("Straße") == search_key("STRASSE"))

The order and normalization form depend on the domain. Preserve the original and do not treat a simplified key as proof of identity.

Confusable characters and security

Normalization does not merge every visually similar character. Latin, Greek, and Cyrillic letters can remain distinct while looking alike. Homograph attacks affect usernames, domains, package names, and identifiers.

Use script restrictions, specialized confusable detection, and human review for sensitive contexts.

Preparing a search key

def prepare_search(text):
    text = unicodedata.normalize("NFKC", text)
    text = text.casefold()
    return " ".join(text.split())

Version this transformation as part of the search-index schema because Unicode database upgrades may change results.

Common mistakes

  • Comparing text without a chosen normalization form.
  • Using NFKC where original distinctions matter.
  • Replacing original content with an accent-stripped copy.
  • Treating every numeric character as an ASCII digit.
  • Expecting normalization to prevent homographs.
  • Using East Asian width as a complete display-width calculation.
  • Ignoring the Unicode database version.
  • Always preserve original input.
  • Define normalization per field and purpose.
  • Normalize both sides of a comparison.
  • Version derived keys and indexes.
  • Test relevant languages and scripts.
  • Use casefold() for caseless matching.
  • Apply additional controls to sensitive identifiers.

Continue with Python textwrap, Python locale, Python fnmatch, Python pydoc, and Python linecache.

See the official unicodedata documentation and the Unicode HOWTO.

Conclusion

unicodedata enables explicit, reproducible handling of international text. Normalization improves search and comparison, but it does not replace language, identity, security, or presentation policies. Preserve the source and choose transformations according to the domain.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Developer configuring a Python HTTPS server and TLS certificate
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: Build a Local HTTPS Server in Python

    Learn Python HTTPSServer for local HTTPS services, TLS certificates, threaded handling, testing, and practical security limits.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Protected files representing secure TAR extraction with Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: Extract TAR Safely

    Learn Python tarfile extraction_filter for safer TAR extraction, path validation, links, permissions, and resource limits.

    Ler mais

    Tempo de leitura: 5 minutos
    08/10/2026
    Python code showing warnings controlled with catch_warnings
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: Capture Python Warnings in Tests

    Learn Python catch_warnings to capture, test, and control warnings with focused filters and safe temporary scopes.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Python code representing persistent pickle references
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: Serialize External References

    Learn Python pickle persistent_id for stable external references, validation, security, performance, and long-term compatibility.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Binary code representing Python buffers and memory views
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: Count Values Without Buffer Copies

    Learn Python memoryview.count to count bytes and values in buffers without copies, with formats, limits, and practical safety.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026
    Python code and type annotations
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: Avoid Circular Imports in Annotations

    Learn Python annotationlib to inspect deferred annotations, avoid circular imports, and build safer runtime tooling.

    Ler mais

    Tempo de leitura: 5 minutos
    06/10/2026