Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
326: Unicode Normalization for Text Comparison
I've seen some of the most frustrating bugs in production happen because a developer assumed that if two strings look exactly the same on a screen, they are the same string in Python. It seems intuitive, right? If you see "Café" and "Café", they should be equal. But in the world of Unicode, that's a dangerous assumption.
Visually Identical Doesn't Mean Digitally Equal
Here is the concrete proof. Let's look at the character 'é'. In Unicode, there are actually two different ways to represent this character: as a single precomposed character, or as a base 'e' followed by a "combining acute accent" character. To your eyes, and your terminal, they are indistinguishable.
import unicodedata
# This is the precomposed 'é' (U+00E9)
s1 = '\u00e9'
# This is 'e' (U+0065) followed by the combining accent (U+0301)
s2 = '\u0065\u0301'
print(f"String 1: {s1}")
print(f"String 2: {s2}")
print(f"Are they equal? {s1 == s2}")
# Result: Are they equal? False
If you were writing a login system or a search filter and a user pasted a name from a PDF (which often uses decomposed characters) while your database stored precomposed characters, your code would tell the user their account doesn't exist, even though the spelling is perfect. It's a nightmare to debug because when you print() the variables, they look identical.
Bringing Strings to a Common Baseline with Normalization
To fix this, we have to "normalize" the text. This essentially means picking one of the Unicode standards and forcing both strings to adhere to it before we compare them. In Python, we use the unicodedata module for this. You'll mostly encounter two forms: NFC (Canonical Composition) and NFD (Canonical Decomposition).
- NFC: This takes decomposed characters and "composes" them into a single character if one exists. This is generally what you want for web forms and database storage.
- NFD: This breaks everything down. It takes that single 'é' and turns it into 'e' + 'accent'. This is actually very useful if you want to strip accents entirely (by decomposing them and then filtering out the non-spacing mark characters).
Here is how I handle this in my own projects to ensure comparisons are reliable:
import unicodedata
def compare_strings(str1, str2):
# I normalize both to NFC to ensure we are comparing apples to apples
norm1 = unicodedata.normalize('NFC', str1)
norm2 = unicodedata.normalize('NFC', str2)
return norm1 == norm2
s1 = '\u00e9'
s2 = '\u0065\u0301'
if compare_strings(s1, s2):
print("Now they match!")
else:
print("Still not matching.")
My rule of thumb? Normalize your input as soon as it enters your system. Don't wait until the comparison step. If you normalize everything to NFC at the API boundary, you don't have to pepper unicodedata.normalize calls throughout your business logic, which keeps the code much cleaner.
📋 Practical Task
Building a Normalization-Aware User Search Filter
You are building a search feature for a member directory. The directory contains a list of names, some of which were entered via a MacOS system (which tends to use NFD/decomposed characters) and some via Windows (which tends to use NFC/precomposed characters). Your current search is failing to find matches for names with accents.
Your Task: Implement a function search_members(query, member_list) that returns a list of all members whose names match the query, regardless of how the Unicode characters are represented.
import unicodedata
def search_members(query, member_list):
# Your code here:
# 1. Normalize the query string.
# 2. Iterate through the member_list, normalizing each name.
# 3. Return the list of matching names.
pass
# Test Case
members = [
"René", # Precomposed (NFC)
"Sébastien", # Decomposed (NFD) - \u0053\u0065\u0301bastien
"Alice", # No accents
]
# The query uses a different normalization than some of the list items
query_1 = "Ren\u0065\u0301" # Decomposed 'é'
query_2 = "S\u00e9bastien" # Precomposed 'é'
print(f"Search 1: {search_members(query_1, members)}") # Should find ['René']
print(f"Search 2: {search_members(query_2, members)}") # Should find ['Sébastien']
There are no comments for now.