xmltodict
Turns XML into ordered Python dicts and back again, using a fast streaming Expat parser instead of a DOM.
Repository Health
Technical Analysis
xmltodict is a small, dependency-free Python module that converts XML documents into native Python dictionaries (and back) so developers can work with XML the way they already work with JSON. Rather than building a DOM tree and walking it, it drives the stdlib expat SAX parser directly through a custom handler, keeping memory bounded even for very large documents.
Beyond the basic parse()/unparse() round trip, it supports namespace expansion and collapsing, selective force_list/force_cdata rules (including callables for complex logic), a streaming mode via item_depth/item_callback for gigabyte-scale files like Wikipedia or Discogs dumps, optional comment preservation, and entity-parsing protection against XXE by default.
What You Get
parse()— converts an XML string, file-like object, or generator of chunks into a nested Python dict, with attributes exposed under an@-prefixed key and text under#textunparse()— serializes a Python dict back into an XML document, including pretty-printing, custom indentation, and attribute/text-key conventions- Streaming mode (
item_depth+item_callback) that emits and discards items as they’re parsed, so huge XML dumps never have to fit in memory at once - Namespace handling via
process_namespacesand anamespacesmapping to expand, collapse, or drop XML namespace URIs - Selective
force_listandforce_cdatacontrols (booleans, key tuples, or callables) to normalize inconsistent XML shapes into predictable dict structures - XXE protection by default —
disable_entities=Truerejects external entity declarations unless explicitly turned off
Common Use Cases
- Consuming legacy SOAP/XML APIs or config formats as ordinary Python dicts instead of hand-writing ElementTree traversal code
- Streaming very large XML exports (Wikipedia dumps, Discogs data, log archives) through a fixed-memory pipeline via
item_callback - Round-tripping structured data to XML for systems that require it as an interchange format, using
unparse() - Normalizing XML that sometimes repeats an element and sometimes doesn’t, using
force_listso downstream code can always assume a list
Under The Hood
Architecture
The whole library lives in a single ~658-line module built around _DictSAXHandler, a stateful SAX handler that tracks a path/stack of (name, attrs) tuples plus a text-data buffer as Expat callbacks fire; parse() wires expat.ParserCreate directly to that handler’s startElement/endElement/characters/comments methods and returns handler.item once parsing completes, or streams results through item_callback (raising ParsingInterrupted to stop early) when item_depth is set. The reverse path (_emit/unparse) walks a nested dict recursively and re-emits the same SAX-event vocabulary into a custom _XMLGenerator (a subclass of xml.sax.saxutils.XMLGenerator), so parsing and serialization share one mental model without a DOM ever existing in memory.
Tech Stack
Pure Python 3.9+ with zero runtime dependencies — everything is stdlib: xml.parsers.expat for parsing, xml.sax.saxutils/xml.sax.xmlreader for the reverse serialization path, io.StringIO for buffering unparse() output, and inspect.isgenerator to accept streamed XML chunks. Packaging is a minimal pyproject.toml (setuptools backend, py-modules=["xmltodict"]) with pytest/pytest-cov declared only as an optional test extra; tox.ini fans the test matrix across Python 3.9–3.14 plus PyPy, and GitHub Actions handle testing, release-please-driven versioning, and OIDC-based PyPI publishing.
Code Quality
The two test files (test_xmltodict.py, test_dicttoxml.py) together run to roughly 1,500 lines — more than double the implementation — covering namespaces, streaming/item_depth, force_list/force_cdata callables, comment round-tripping, and encoding edge cases; pytest-cov is wired through tox for coverage. Error handling is deliberate rather than swallowed: parse() raises ValueError when entity declarations are attempted with disable_entities on (XXE hardening), and unparse() raises explicit ValueErrors for multi-root documents or invalid element/attribute names via dedicated _validate_name/_validate_comment helpers. Naming is consistent snake_case throughout; there are no type hints anywhere in the module, and style is enforced only via a pre-commit hook for conventional commits, not a linter or formatter.
What Makes It Unique
The specific choice that sets xmltodict apart is going straight to a streaming Expat SAX handler instead of building and walking a DOM the way ElementTree- or lxml-based converters typically do — memory stays bounded even for gigabyte-scale documents, which the README calls out explicitly for dumps like Wikipedia or Discogs. On the reverse path, unparse() reuses the exact same SAX-event vocabulary to serialize dicts back to XML, giving genuine round-tripping (attributes via an @ prefix, text via #text, comments via #comment) that many one-directional “dump a dict as XML” utilities don’t attempt. It isn’t algorithmically novel so much as an unusually disciplined, single-purpose application of the stdlib SAX API — and its longevity as the de facto standard for XML-as-dict in Python since 2012 reflects how well that tradeoff has held up.
Used by 4 apps in this directory
ClickHouse
Databases · Analytics · Data Engineering
Open-source column-oriented database that delivers real-time analytical queries on petabyte-scale data with millisecond latency.
OpenBB
Databases · Analytics · Invoicing Finance
The AI Workspace for Finance: Connect Data, Run AI Agents, Build Analytics
SWIRL
Search · Databases · Data Engineering
Federated AI search and RAG across 100+ enterprise sources—no data extraction, no vector database required.
Timeplus Proton
Data Engineering · Analytics
Single C++ binary SQL engine for real-time stream processing, ETL, and analytics on Kafka, Redpanda, and ClickHouse with sub-millisecond latency.