XML in Production · Data Correctness
Müller Became Müller
Customer names with accents came out garbled after an XML export was decoded with the wrong encoding, then saved and sent on to other systems.
Scenario
Example team: a CRM sync service downloads customer records as XML over HTTP, converts them to strings, adds a field, and posts the result to a billing system. The upstream system sends ISO-8859-1 and says so in the XML declaration.
What you see in production
Customer "Müller" appears as "Müller" in invoices; "Zoë" becomes "Zoë". Support tickets arrive from Germany and France. Some records fail to parse at all with "Invalid bytes in character encoding".
The naive code
text = response.content.decode("utf-8", errors="replace") # guess the encoding
doc = etree.fromstring(text.encode("utf-8")) # declaration now lies
Reproduce it
from lxml import etree
payload = '<?xml version="1.0" encoding="ISO-8859-1"?><customer>Müller & Zoë</customer>'.encode("iso-8859-1")
# Right: give the parser the bytes; it reads the declaration
print("bytes to parser:", etree.fromstring(payload).text)
# Wrong 1: decode as UTF-8 first (bad bytes are replaced)
guessed = payload.decode("utf-8", errors="replace")
print("decoded as UTF-8:", guessed[guessed.index("<customer>") + 10:guessed.index("</customer>")])
# Wrong 2: decode as Latin-1, re-encode as UTF-8, but keep the old declaration -> classic mojibake
text = payload.decode("iso-8859-1").encode("utf-8")
print("declaration now lies:", etree.fromstring(text).text)
Output
bytes to parser: Müller & Zoë decoded as UTF-8: M�ller & Zo� declaration now lies: Müller & Zoë
Root cause
XML carries its own encoding in the declaration, and the parser is the component that should apply it. Decoding the bytes yourself means guessing. Re-encoding without updating the declaration then makes the parser decode UTF-8 bytes as Latin-1, turning each two-byte UTF-8 character into two wrong characters (the "Ã" pattern).
The fix
- Pass bytes to the XML parser (
etree.fromstring(response.content)), not decoded strings. - Write output as UTF-8 with a matching declaration (
etree.tostring(doc, encoding="UTF-8", xml_declaration=True)). - If an HTTP
Content-Typecharset disagrees with the declaration, decide on one rule (forapplication/xml, the charset parameter wins per RFC 7303) and log mismatches. - Add test fixtures with accented and non-Latin text (é, ß, ł, 中文) to every integration.
Trade-offs
- Some legacy partners declare one encoding and send another. A repair step may be needed, but quarantine those messages instead of silently "fixing" everything.
- Normalising everything to UTF-8 internally is simpler than supporting many encodings end to end.
Detect it in production
- Search stored data for mojibake patterns (
ü,é,’) and for the replacement character�. - Count parser errors about encodings per partner.
Key points
- Let the XML parser decode bytes using the declaration; never decode first.
- Output UTF-8 with a correct declaration.
- Test every integration with non-ASCII data.
Practice questions
Why should you pass bytes rather than a decoded string to an XML parser?
Answer
The XML declaration tells the parser which encoding the bytes use. If you decode first, you guess the encoding, and if you re-encode the text, the declaration may no longer match the bytes.
What does 'Müller' tell you about a bug?
Answer
UTF-8 bytes were decoded as Latin-1/Windows-1252: the two-byte UTF-8 sequence for ü became two characters. Find the step that decoded with the wrong encoding.
Open in the app to mark lessons complete, take notes and track your progress.