Every independent way to work with XML this library offers — the core module, objectify, dictify, jsonify (both JSON and JSON Lines), and the command-line tool — each measured on its own against its real, direct competitor.
Real wheel sizes for the current platform and Python, resolved through pip-size's Python API — no downloads, no installs, just querying PyPI's index — summed across every transitive dependency, not just the top-level package. ElementTree ships with Python itself: 0 extra bytes.
| Package | Version | Installed size | Deps | Capability |
|---|---|---|---|---|
| ElementTree | stdlib | 0.0 KB | 0 | dom, streaming iterparse (Python standard library) |
| xmljson | 0.2.1 | 9.9 KB | 0 | lxml/ElementTree tree → dict adapter |
| xmltodict | 1.0.4 | 13.3 KB | 0 | dom → dict (+ streaming callback mode) |
| untangle | 1.3.0 | 29.1 KB | 1 | dom → lazy attribute-style object (read-only) |
| pygixml ★ | 0.12.2 | 402.5 KB | 0 | dom, streaming iterparse, objectify, dictify, jsonify, cli |
| yq | 4.2.0 | 917.6 KB | 4 | cli: jq-style query/convert (installs the `xq` XML binary used below) |
| lxml | 6.1.3 | 5120.0 KB | 0 | dom, streaming iterparse, objectify |
Sizes measured with pip-size (lower is better). Deps count is transitive, flattened.
The two fundamental ways to read XML: load the whole thing into memory at once, or walk it one element at a time without ever holding the whole document. Time per corpus entry (best of 7 runs) and peak memory (each measured in its own isolated process).
Directly building an in-memory tree from the XML text. xmltodict/xmljson have no separate “parse to a tree” step distinct from “parse straight to dict”, so they don't appear here.
pygixml is the reference (fastest most consistently across the corpus). On average (E[time/pygixml], mean of each entry's own ratio, not a ratio of medians): lxml 8.0×, ElementTree 12.7× as long.
ElementTree uses the least memory (uses the least memory most consistently across the corpus). On average (E[memory/ElementTree]): pygixml 1.1×, lxml 1.4× as much.
Streamed via pygixml.iterfind, against lxml's and ElementTree's own iterparse -- real streaming APIs on both sides. Only runs on corpus entries with one uniformly repeated element (a config-tree genre genuinely has none, so it's absent here, not skipped by oversight).
lxml is the reference (fastest most consistently across the corpus). On average (E[time/lxml], mean of each entry's own ratio, not a ratio of medians): ElementTree 1.2×, pygixml 2.4× as long.
ElementTree uses the least memory (uses the least memory most consistently across the corpus). On average (E[memory/ElementTree]): pygixml 1.2×, lxml 1.5× as much.
Lazy attribute-style access (root.child.grandchild), matched against lxml's own objectify submodule -- a real feature both libraries ship, not an improvised comparison.
pygixml is the reference (fastest most consistently across the corpus). On average (E[time/pygixml], mean of each entry's own ratio, not a ratio of medians): lxml 7.8× as long.
pygixml uses the least memory (uses the least memory most consistently across the corpus). On average (E[memory/pygixml]): lxml 1.3× as much.
Two independent comparisons, not one: dictify's regular (DOM) mode against
xmltodict.parse, and its streaming mode (dictify.iterdict)
against xmltodict's own streaming mode (item_depth +
item_callback) — xmltodict genuinely supports both, so
neither comparison is a DOM tool facing off against a streaming-only one.
Both sides use the same convention (@-prefixed attributes, #text for mixed content) so the dicts they produce are structurally identical, not just “both happen to be a dict.”
pygixml is the reference (fastest most consistently across the corpus). On average (E[time/pygixml], mean of each entry's own ratio, not a ratio of medians): xmltodict 6.9× as long.
pygixml uses the least memory (uses the least memory most consistently across the corpus). On average (E[memory/pygixml]): xmltodict 1.2× as much.
xmltodict genuinely supports streaming too (item_depth + item_callback), not just a DOM-only tool being compared unfairly against a streaming one. Same record-tag restriction as iterparse.
pygixml is the reference (fastest most consistently across the corpus). On average (E[time/pygixml], mean of each entry's own ratio, not a ratio of medians): xmltodict 1.6× as long.
pygixml uses the least memory (uses the least memory most consistently across the corpus). On average (E[memory/pygixml]): xmltodict 1.3× as much.
Solid line = streaming mode, dashed = DOM mode — is streaming actually worth it, and for whom?
Two independent output shapes, not one: a single JSON document for the whole input, or one JSON object per record, streamed out as JSON Lines.
XML string in, one JSON document out. ElementTree has no built-in dict/JSON conversion at all -- a real gap for it, not an oversight in this chart.
On small documents, memory isn't the deciding factor for either approach -- both fit comfortably in RAM. What's left is speed, and there jsonify.dumps (the DOM path) wins clearly: on average 10.4× the speed of xmljson (E[timexmljson/timepygixml], mean of each small entry's own ratio). Even on memory -- where this is the DOM path, not stream_dump, so both sides are genuinely O(n) here, a fair same-complexity-class comparison -- pygixml still uses on average 1.2× less memory than xmljson on these same small files (E[memoryxmljson/memorypygixml]).
Once the document stops being small, the calculus changes: that's exactly when jsonify.stream_dump takes over, trading the DOM path's speed for a fundamentally different complexity class. pygixml (stream_dump) (O(1)) stayed within 15.7–15.7MB across every size tested, up to 62.7MB. See the memory panel below for the full trade-off, not just the flattering half.
pygixml is the reference (fastest most consistently across the corpus). On average (E[time/pygixml], mean of each entry's own ratio, not a ratio of medians): xmljson 10.3×, xmltodict 10.8× as long.
pygixml uses the least memory (uses the least memory most consistently across the corpus). On average (E[memory/pygixml]): xmltodict 1.3×, xmljson 1.3× as much.
The streaming variant of this same JSON output, jsonify.stream_dump, never
holds the whole document in memory: its curve stays flat — O(1)
against every DOM-based approach's O(n). That's a difference in growth rate, not
a fixed multiplier: no single "N× less memory" number at one input size represents
it fairly — the chart does. Largest file in this comparison: 62.7MB.
pygixml (stream_dump) (O(1)) stayed within 15.7–15.7MB across every size tested, up to 62.7MB. Among the O(n), DOM-based approaches only (a fair comparison -- same complexity class): pygixml (DOM) is the reference (uses the least memory most consistently across the sizes tested); on average (E[memory/pygixml (DOM)], mean of each size's own ratio): xmltodict 1.0×, lxml + xmljson 2.1× as much.
Best of 3 timed repeats per process, after one untimed warm-up call.
stream_dump writes optimistically and patches the output file in place once
it learns a tag repeats — O(n) for the shape almost all real giant XML actually has
(one repeated tag per nesting level). We also generate the one shape where it isn't: two
different tags repeating and interleaving at the same level, shown here on purpose, not
just cited in the docs.
N grew 256× (500 → 128,000): records-shape time grew 261.6× (linear, tracks N); interleaved-shape time grew 1343.8× (superlinear, approaching the documented O(n²) case). The slope, not a single ratio, is the finding.
Everyone's roughly-linear time on the realistic shape, at moderate N.
The streaming sibling of the panel above: instead of one JSON value for the whole document, one JSON object per matching element, read one at a time. xmltodict's own item_depth/item_callback streaming mode is the fair reference here too -- json.dumps happens per callback, same as pygixml emits per element. Same uniformly-repeated-tag restriction as streaming/iterparse above.
xmltodict is the reference (fastest most consistently across the corpus). On average (E[time/xmltodict], mean of each entry's own ratio, not a ratio of medians): pygixml 1.2× as long.
python -m pygixml jsonify against xq .
(installed by pip install yq) — real subprocess invocations end to end,
process startup included on both sides, best of 5 runs.
On average (E[timexq/timepygixml], mean of each corpus entry's own ratio): pygixml's CLI takes 3.5× xq's time, i.e. xq takes 0.3× as long, across 12 corpus entries.
Every number above came from this one machine. Recorded automatically by
system_info.py at run time — not filled in by hand.
| CPU | Intel(R) Xeon(R) Processor @ 2.10GHz |
| Logical cores | 1 |
| RAM | 3.9 GiB |
| Architecture | x86_64 |
| OS | Linux 6.18.44-fc-v42 |
| Python | CPython 3.12.3 |