Normalize Unicode Text for Search Keys
Create a documented comparison key using Unicode NFC normalization and casefold.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Normalize Unicode Text for Search Keys
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
A search box says it cannot find a name that appears to be typed exactly as stored. Both versions look like Café on screen, yet Python equality returns False. One version may contain a single precomposed accented character; another may contain an ordinary e followed by a combining accent. Both are valid Unicode text. A second surprise appears with case- insensitive search: lowercasing Straße and STRASSE still gives different strings, while Python's casefold method can treat this pair as equivalent. In this lesson we will reproduce both misses, inspect the difference between
- 00:40
lower and casefold, and build a comparison key using NFC normalization. We will preserve the original display text rather than overwriting it with a key. We will also show a non-match that must remain a non-match: unaccented cafe and café. Normalization is a matching policy, not permission to erase every distinction or to decide that two people are the same. The companion website exercise checks composed accents, case variants, surrounding whitespace, and unsupported input types. Run the code along with the video and predict each Boolean before revealing it. The first comparison is False.
- 01:20
Looking at the next two lines explains why. Lowercasing Straße leaves its sharp s, giving straße. Lowercasing STRASSE gives strasse. This is not a defect in str.lower; lowercasing and full Unicode caseless matching have different purposes. Python provides str.casefold for more aggressive caseless comparison. It can map the sharp s to ss. Before replacing lower, however, think about what the key will be used for. A search index can deliberately merge these case variants. A unique account identifier or a legal name might need a stricter or locale- specific rule, and a product should not silently rewrite the displayed spelling.
- 02:03
The function here will return a derived key, while the original string stays available for display. Lower also does nothing about the two code-point sequences that can render as Café. A successful fix has to handle both case and canonical representation, and the tests must include both. Simply making this one German example pass would leave the accent example broken. The first two words can look identical while containing different code points. Python string equality checks the sequence, so the first answer is False. NFC normalization composes canonically equivalent sequences where possible, and the comparison becomes True.
- 02:46
The third line is intentionally False. An accent is meaningful information; NFC does not strip it from café or add it to cafe. That distinction makes canonical normalization a narrower and safer rule than blindly deleting combining marks. It still does not solve every human- language search question. Different scripts, locale rules, punctuation, transliteration, and visually confusable characters require separate policies. For this lesson we will keep the contract small: trim surrounding whitespace, perform caseless matching, and compose canonically equivalent text. We will not collapse internal spaces, remove accents, or claim that a comparison
- 03:28
key is a secure identity. Notice also that the original strings were never changed. The normalizer returned new strings for comparison, leaving the input display forms available to the application. The function first rejects values that are not strings. Silently converting None to the text None would create a misleading searchable entry. It strips whitespace only at the edges; two spaces inside a name remain two spaces. Next, casefold performs Unicode caseless mapping. NFC normalizes the resulting sequence so that composed and decomposed forms match under this key.
- 04:09
The first printed comparison includes uppercase letters, surrounding spaces, and the two accent representations; the result is True. The second demonstrates the sharp-s mapping; it is also True. The last confirms that the key preserves the accent distinction between cafe and café. The exact order of transformations is a policy choice. Normalizing after case folding makes the final key canonically composed even if the case operation changes code points. For a large search product, you may add indexing, tokenization, or locale-aware ranking, but do not smuggle those behaviors into this compact function.
- 04:51
A stable key is useful only when callers know what it equates and what it keeps distinct. On the website, the starter uses lower alone. Replace it with this explicit sequence and then run the practical checker. The first two assertions verify the matching behavior we intentionally chose. The third protects an equally important boundary: café must not collapse into cafe under NFC and case folding. If a future developer adds accent removal to the function, this test would fail and force a discussion about the changed policy. The fourth assertion confirms that strip only trims the ends and leaves internal spacing alone.
- 05:32
A different product might normalize all whitespace, but that could merge names or phrases unexpectedly. The final try block makes the type contract visible; None raises TypeError rather than creating a string-shaped key. When the script prints checks passed, we have evidence for both positive and negative cases. There are still limits. A search key is not a complete spelling- correction system, and equal keys do not prove that two records refer to the same person. For account security, validation and identifier policy need separate design. Try adding a decomposed accent after a different letter and predict whether NFC creates an equivalent composed form.
- 06:15
Then compare the original string with its returned key and note that the original remains untouched. On the companion page, use Try this example to load each sample into the console, finish the checked task, and answer the quiz about canonical equivalence, accent preservation, and case folding.
Understand the concept
Text that looks the same on screen can be stored as different sequences of Unicode code points. The word Café may contain one precomposed é character, or an e followed by a combining accent. Python string equality compares their code-point sequences, so those two spellings can compare unequal. This is not an encoding failure: both are valid Unicode strings. Before a search or deduplication rule treats them as the same, it needs a deliberate normalization form.
unicodedata.normalize('NFC', text) composes canonically equivalent sequences into a common representation where possible. It does not erase accents. The unaccented word cafe remains different from café. That matters because accent removal changes information and may merge distinct names. For search, a product might choose accent-insensitive matching, but that is a separate, explicitly documented requirement rather than a side effect of NFC.
Lowercasing is not a complete caseless comparison. str.casefold applies a more aggressive Unicode caseless mapping: German Straße becomes strasse, so it matches STRASSE under this rule. Normalization can be applied after casefold to make the final key canonically composed. The exercise also trims surrounding whitespace; internal spaces remain untouched. The function returns a comparison key and never edits the original display string.
The task requires a string input, then strip, casefold, and NFC normalization. Its tests compare composed and decomposed accents, German sharp s, surrounding whitespace, and a deliberately different unaccented word. A comparison key is not a secure identifier, a substitute for locale-specific collation, or proof that two people have the same name. Store original text alongside any key and state the matching policy to users.
- Visually identical Unicode may have different code points
- NFC composes canonical equivalents
- casefold is stronger than lower for caseless matching
- A search key does not replace original display text
See it step by step
Read the code, predict the output, then compare it with the result.
01. Expose two spellings of one visible word
import unicodedata
a = 'Café'
b = 'Cafe' + chr(0x301)
print(a == b)
print(unicodedata.normalize('NFC', a) == unicodedata.normalize('NFC', b))False
TrueNFC makes canonically equivalent code-point sequences comparable without removing the accent.
02. See a difference between lower and casefold
print('Straße'.lower() == 'STRASSE'.lower())
print('Straße'.casefold() == 'STRASSE'.casefold())False
Truecasefold maps sharp s to ss for caseless matching; lower does not.
03. Keep an accent as meaningful data
import unicodedata
print(unicodedata.normalize('NFC', 'cafe') == unicodedata.normalize('NFC', 'café'))
print(unicodedata.normalize('NFC', 'Cafe' + chr(0x301)))False
CaféCanonical normalization composes equivalent accents but does not make accented and unaccented words equal.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Reveal two representations of one visible word
The two strings look like the same word on screen. The first contains a precomposed é, while the second appends a combining accent after e. Python compares code-point sequences, not rendered glyphs, so == returns False. This matters when a search index or a deduplication routine receives text from different keyboards or sources.
Normalize both strings to NFC and compare again. NFC composes canonically equivalent sequences where possible and returns matching results for this pair. The transformation does not delete the accent or modify the original variables. Keep the original text for display; derive a comparison key only where a documented matching rule calls for one.
import unicodedata
a = 'Café'
b = 'Cafe' + chr(0x301)
print(a == b)
print(unicodedata.normalize('NFC', a) == unicodedata.normalize('NFC', b))False
True- The visual rendering can hide different code-point sequences.
- NFC brings canonical equivalents to a common form.
- The originals remain available for display.
Compare lowercasing and case folding
Lowercasing handles many familiar case pairs, but Unicode caseless matching has broader mappings. German Straße lowercases to straße; STRASSE lowercases to strasse. The strings still differ. casefold maps the sharp s to ss, making these particular words equal under a caseless rule.
That broader mapping is useful for search keys, yet it is not a locale-aware name comparison or a secure account-identity rule. Different spellings can converge under normalization and folding. Explain the matching policy to users, and do not discard the source spelling after producing a search key. The next stage adds NFC so composed accents also align.
print('Straße'.lower() == 'STRASSE'.lower())
print('Straße'.casefold() == 'STRASSE'.casefold())False
True- lower does not turn sharp s into ss.
- casefold does for caseless matching.
- A search key has a narrower purpose than a display name.
Build a comparison key without erasing accents
The finished function accepts only text, removes surrounding whitespace, case-folds, and normalizes the result to NFC. The order makes the returned string a stable comparison key for the examples in this lesson. Internal spaces remain unchanged. A non-string input raises TypeError rather than silently becoming the text 'None' or '4'.
Verify both desired matches and a deliberate non-match. Composed and decomposed Café should match, as should Straße and STRASSE, but unaccented cafe must remain different from café. If a product later wants accent-insensitive search, it needs a separate rule, careful examples, and an understanding of name collisions.
import unicodedata
def search_key(text):
if not isinstance(text, str):
raise TypeError('text must be str')
return unicodedata.normalize('NFC', text.strip().casefold())
print(search_key(' CAFÉ ') == search_key('Cafe' + chr(0x301)))
print(search_key('Straße') == search_key('STRASSE'))
print(search_key('cafe') == search_key('café'))True
True
False- Reject unsupported input types.
- Fold and normalize into a key.
- Preserve the accent distinction while matching canonical equivalents.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Import unicodedata. Use isinstance(text, str), strip surrounding spaces, call casefold, then normalize the result with 'NFC'.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Using lower alone for all case variants
return text.strip().lower()return unicodedata.normalize('NFC', text.strip().casefold())What happens: Straße and STRASSE yield different comparison keys.
casefold handles broader Unicode caseless mappings and NFC resolves canonical forms.
Removing accents without a product rule
return ''.join(c for c in text if not unicodedata.combining(c))return unicodedata.normalize('NFC', text)What happens: Different written names or words can collapse into the same key.
Canonical equivalence preserves meaningful accent distinctions.