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

    Server network representing resource management with Python ExitStack
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ExitStack: Manage Resources

    Learn Python ExitStack to manage dynamic files, connections, callbacks, and cleanup safely in predictable reverse order.

    Ler mais

    Tempo de leitura: 5 minutos
    11/08/2026
    Locked folder representing file types and permissions with Python stat
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python stat: File Types and Permissions

    Learn Python stat to interpret file types, permissions, links, timestamps, Windows attributes, and Unix flags safely and portably.

    Ler mais

    Tempo de leitura: 5 minutos
    10/08/2026
    Laptop with code representing automatic documentation with Python pydoc
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python pydoc: Automatic Documentation

    Learn Python pydoc to generate terminal help, HTML, search, and a local documentation server safely from docstrings.

    Ler mais

    Tempo de leitura: 6 minutos
    10/08/2026
    International keyboard representing numbers, currency, and dates with Python locale
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python locale: Numbers, Currency, and Dates

    Learn Python locale to format and parse numbers, currency, dates, encodings, and cultural sorting without concurrency mistakes.

    Ler mais

    Tempo de leitura: 6 minutos
    09/08/2026
    Monitor and network representing system information with Python platform
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python platform: System Information

    Learn Python platform to identify operating systems, architecture, Linux distributions, Python versions, and runtime environments safely.

    Ler mais

    Tempo de leitura: 6 minutos
    09/08/2026
    Source code and compiler representing build paths and variables with Python sysconfig
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python sysconfig: Paths and Build Info

    Learn Python sysconfig to discover installation paths, build variables, headers, virtual environments, and platform tags safely.

    Ler mais

    Tempo de leitura: 7 minutos
    09/08/2026