xmltodict

Turns XML into ordered Python dicts and back again, using a fast streaming Expat parser instead of a DOM.

Library
PyPI
v1.0.4
5,752stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
64/100Good
Development Activity56
Maintenance32
Community68
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
66/100Good
Architecture78
Code Quality72
Innovation65
Learning Curve50

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 #text
  • unparse() — 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_namespaces and a namespaces mapping to expand, collapse, or drop XML namespace URIs
  • Selective force_list and force_cdata controls (booleans, key tuples, or callables) to normalize inconsistent XML shapes into predictable dict structures
  • XXE protection by default — disable_entities=True rejects 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_list so 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.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search