mrkeyoor.com_
Sun 20 Sept 12:46 UTC
PyPIDataupdated 20 Sept 2026

xmltodict review

xmltodict 1.0.4 turns XML elements into nested Python dictionaries and lists, with @-prefixed attribute keys and #text where text shares an element with metadata. unparse() converts that representation back to XML, and item_callback can process records at a chosen depth without building the whole document. This mapping is convenient for predictable API payloads, but it loses XML details such as mixed-content order and exact comment placement. Version 1.0.4 adds bytes_errors to unparse() and handles byte scalar decoding consistently. Our import of the pure Python package completed in 0.22 seconds.

Verdict

Our xmltodict 1.0.4 install took 0.3 seconds, added one package, and used 1 MB, so the dependency cost is tiny for predictable XML payloads. Walk away when schema validation, XPath, or exact document fidelity is part of the job.

We installed it

Lab card: what happened when we installed xmltodictScreenshot of xmltodict documentation
Install✓ · 0.3s1 package on disk · 1 MB
Importimport xmltodict in 0.22s · pure Python · requires Python >=3.9
Known vulns0(pip-audit)

Answers from our run

Does xmltodict install cleanly?

Yes. In a fresh container with an empty cache, pip install xmltodict finished in 0.3s, leaving 1 package and 1 MB on disk. pip-audit reported no known vulnerabilities.

What does xmltodict need to run?

Python >=3.9, and nothing compiled: it is pure Python. In our run import xmltodict succeeded in 0.22s.

xmltodict or lxml: which should you use?

lxml: Choose it for XPath, XSLT, schemas, and full tree control. Our xmltodict 1.0.4 install took 0.3 seconds, added one package, and used 1 MB, so the dependency cost is tiny for predictable XML payloads.

When should you not use xmltodict?

You need XPath, XSLT, XSD validation, or direct tree edits; xmltodict has no API for those operations

API stability4/5parse() and unparse() remain the central functions, and options such as force_list, force_cdata, process_namespaces, postprocessor, and item_callback cover the same mapping model. Version 1.0 raised the Python floor, while 1.0.3 changed None serialization and 1.0.4 made byte scalar handling consistent. Basic reads are steady; code that compares generated XML byte for byte should pin the package and test every update.
Docs4/5The GitHub README returned HTTP 200 and contains the full parse and unparse option lists plus working examples for namespaces, streaming, nested lists, force_list, force_cdata, and byte errors. It states that empty lists emit no element and that mixed content, attribute order, and top-level comment order can be lost. Everything sits on one long page, so there is no separate searchable reference for callback signatures or combinations of less common flags.
Maintenance4/5The unarchived repository was last pushed on 2026-08-19 and GitHub showed only 5 open issues and pull requests. Release 1.0.4 shipped on 2026-02-22, one week after 1.0.3, to correct byte scalar serialization and add bytes_errors. Recent releases also adjusted disabled-entity behavior, None values, comments, and selective CDATA handling. The codebase is small, though it appears to depend on a limited maintainer group.
Ecosystem4/5PyPI Stats counted 27,808,381 downloads in the latest week, and GitHub reported 5,751 stars. The mapping is familiar enough to appear in many API adapters, and packages exist for several operating-system repositories. It has no plugin architecture, bundled py.typed marker, XPath layer, or schema engine. types-xmltodict, defusedxml, lxml, and xmlschema solve separate parts of that wider XML toolchain.

Use it if

  • A stable XML API is easier to consume through dictionary keys than tree traversal
  • A large dump contains repeatable records that item_callback can process at one depth
  • The same small adapter must parse XML and emit a simple XML response
  • force_list, namespace mapping, or a postprocessor can normalize the producer's known schema
Skip it if

Setup reality

We installed xmltodict 1.0.4 in 0.3 seconds on Python 3.12. It added 1 package and used 1 MB on disk; pip-audit found 0 known vulnerabilities. The measured metadata contained 2 direct dependency entries, both tied to the test extra. This is pure Python and requires Python 3.9 or newer. import xmltodict worked in 0.22 seconds. The installed distribution has no py.typed marker, and its license metadata was unknown in our inspection.

There is no credential or config file. parse() accepts XML text, a file-like object, or a generator and delegates parsing to Expat. Leaf values remain strings until a postprocessor converts them. Static type support lives in the separate types-xmltodict distribution. Those stubs describe the API, not the dictionary shape of a particular XML schema. Use schema validation elsewhere if field types are part of the contract.

Entity processing is disabled by default. Keep disable_entities=True for input outside your control. That setting addresses entity expansion, while generated element names are a separate concern: unparse() sends dictionary keys to XMLGenerator, and the README says that standard-library writer does not validate them. Do not feed arbitrary user keys into unparse(). Version 1.0.4's bytes_errors controls invalid byte decoding; 'strict' raises instead of replacing bytes.

The common production bug is shape drift. One maps to a scalar and repeated nodes map to a list, so declare force_list for every repeatable element. A text-only node becomes a dictionary once an attribute appears; selective force_cdata can keep #text present. Streaming requires the right item_depth and a callback returning True. unparse() expects one root key, omits empty lists, and cannot restore the order of mixed text and children.

Patterns

Read elements and attributes parse-string

import xmltodict

xml = '<order id="42"><item>paper</item><item>ink</item></order>'
doc = xmltodict.parse(xml)
print(doc['order']['@id'])
print(doc['order']['item'])

Attribute and leaf values remain strings. Two item siblings produce a list.

Read text beside an attribute attribute-text

doc = xmltodict.parse('<price currency="USD">19.99</price>')
price = doc['price']
print(price['@currency'])
print(price['#text'])

Without the attribute, price would be a string instead of a dictionary with #text.

Keep repeated fields as lists force-list

doc = xmltodict.parse(
    '<catalog><item>paper</item></catalog>',
    force_list=('item',),
)

Choose repeatable names from the schema. A single sample cannot tell you whether a second item may arrive.

Keep a text node dictionary-shaped force-cdata-shape

doc = xmltodict.parse(
    '<root><value>42</value></root>',
    force_cdata=('value',),
)
print(doc['root']['value']['#text'])

Selective force_cdata preserves the #text shape even when the current node has no attribute.

Convert known leaf types postprocess-values

def convert(path, key, value):
    if key == 'quantity':
        return key, int(value)
    return key, value

result = xmltodict.parse(source, postprocessor=convert)

The callback sees each completed key and its ancestor path. Returning None drops that key.

Process a large dump by depth stream-records

from gzip import GzipFile

def handle(path, record):
    save_record(record)
    return True

with GzipFile('records.xml.gz') as source:
    xmltodict.parse(source, item_depth=2, item_callback=handle)

Streaming mode does not return the document. False from the callback stops parsing.

Shorten namespace names namespace-map

namespaces = {
    'https://example.com/core': None,
    'https://example.com/audit': 'audit',
}
doc = xmltodict.parse(source, process_namespaces=True, namespaces=namespaces)

A namespace absent from the map remains expanded in the resulting key.

Generate a simple XML document unparse-dictionary

payload = {'response': {
    '@version': '2',
    'status': 'ok',
    'item': ['paper', 'ink'],
}}
xml = xmltodict.unparse(payload, pretty=True)

The outer mapping needs one root key. A value of [] emits no element.

Write XML to a stream write-output

with open('response.xml', 'w', encoding='utf-8') as output:
    xmltodict.unparse(payload, output=output, pretty=True)

Passing output writes incrementally to that file-like object instead of returning a string.

Reject invalid byte values byte-errors

xml = xmltodict.unparse(
    {'root': {'value': raw_bytes}},
    bytes_errors='strict',
)

Version 1.0.4 added bytes_errors. strict raises where the default replace policy substitutes characters.

Retain surrounding text whitespace preserve-whitespace

doc = xmltodict.parse(
    source,
    strip_whitespace=False,
    cdata_separator=' ',
)

pretty=True later adds indentation and newlines, so it is a poor match for exact whitespace preservation.

Keep entity parsing disabled disable-entities

doc = xmltodict.parse(untrusted_source, disable_entities=True)

This is the default. It does not validate dictionary keys later supplied to unparse().

Alternatives

PackageRegistryPick it when
lxmlPyPIChoose it for XPath, XSLT, schemas, and full tree control.
defusedxmlPyPIChoose it when defensive parsing of hostile XML is the first requirement.
xmlschemaPyPIChoose it when XSD validation and schema-driven conversion are required.
untanglePyPIChoose it when attribute-style object access fits better than dictionary keys.

More data guides

numpy · fsspec · pandas · sqlalchemy · pyarrow · lxml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.