Skip to main content

XML parser with streaming iterator interface

Project description

xml_iterator

Streaming XML for Python. Primary goal: defeat the infinite depth attack on large dumps where useful records sit under outer elements that stay open until late/EOF — a tree/DOM consumer either waits for those outers or holds the whole open tree.

<root>                              ← opens near start
  <Payload>
    <RefData>                       ← open for almost the whole file
      <FinInstrm> ... </FinInstrm>  ← record 1 complete (outers still open)
      <FinInstrm> ... </FinInstrm>  ← record 2
      ...
    </RefData>                      ← closes near EOF
  </Payload>
</root>

Protection is streaming under open wrappers plus user discard / early stop — not max_depth on a full-document dict. Process each record on its end event, drop it, and break after K records. Memory stays bounded only if finished work is discarded.

Threat model / landscape: backlog/docs/streaming-memory-model-and-landscape.md.

Install

PyPI — package xml-iterator, import xml_iterator.

pip install xml-iterator
# or: uv pip install xml-iterator

From a clone: make develop (release extension; needed for honest benches/tests).

Benchmarks

Release builds only (make develop / make build). Debug extensions are ~9× slower.

Numbers: 2026-07-17. Source of truth: benchmark_data/benchmark_results.json (regenerate this section with make readme-benchmarks). Narrative: PERF_2026-07-17.md.

Full-document dict — xml_to_dict vs xmltodict.parse

Same output shape on synthetic / SwissProt (attributes included; namespace prefixes stripped). Full-file tree build — fine for modest docs / parity; streaming is the large-file path.

Synthetic

Elements Size xml_iterator xmltodict Speedup
500 0.2 MB 0.007s 0.036s 5.5×
2,000 0.7 MB 0.032s 0.136s 4.2×
5,000 1.8 MB 0.069s 0.229s 3.3×

Real files

Dataset Size xml_iterator xmltodict Speedup Notes
SwissProt 110 MB 2.573s 13.342s 5.19× results identical
ESMA FIRDS 441 MB 8.451s 54.379s 6.43× results differ (shape)

Early stream exit (stop after 100,000 events on a 50,000-item file): 0.028s vs full xml_to_dict 0.345s (~12×). Any streaming parser gets this; not unique to this library.

Stream backends — same event profile

Comparators in xml_iterator.comparators: xml_iterator, et_iterparse, sax, lxml_iterparse. All yield (count, event, value). make benchmark / make benchmark-all time every backend (or record an explicit skip).

Policy (one stream table per file): full multi-backend drain if size ≤150 MB (e.g. SwissProt); else early exit first 1,000,000 events only (e.g. FIRDS). No redundant early+full on the same file. SAX is N/A for early exit (adapter materializes full parse first). SAX full drain skipped above 20 MB (RAM), not a capability gap.

Synthetic — 20,000 books, full drain (7.1 MB, 500,002 events)

Backend Time Events Rate vs xml_iterator
xml_iterator 0.200s 500,002 2.5M/s 1.00×
lxml_iterparse 0.466s 500,002 1.1M/s 2.33× slower
sax 0.813s 500,002 615k/s 4.06× slower
et_iterparse 1.036s 500,002 482k/s 5.18× slower

SwissProt — full drain (110 MB, 7,967,906 events)

Backend Time Events Rate vs xml_iterator
xml_iterator 2.088s 7,967,906 3.8M/s 1.00×
lxml_iterparse 5.563s 7,967,906 1.4M/s 2.66× slower
et_iterparse 7.496s 7,967,906 1.1M/s 3.59× slower
sax skipped skipped full drain >20MB (adapter buffers all events; RAM)

ESMA FIRDS — early exit first 1,000,000 events (441 MB; full multi-backend drain >150 MB skipped)

Backend Time Events Rate vs xml_iterator
xml_iterator 0.245s 1,000,000 4.1M/s 1.00×
et_iterparse 0.777s 1,000,000 1.3M/s 3.17× slower
lxml_iterparse 0.872s 1,000,000 1.1M/s 3.56× slower
sax skipped N/A early-exit (SAX adapter materializes full parse first)

Reproduce

make benchmark          # synthetic dict + stream + early-exit → JSON
make benchmark-all      # SwissProt + FIRDS → JSON
make show-benchmarks    # pretty-print last JSON (no rebuild)
make readme-benchmarks  # rewrite this section from JSON (no re-run)

Makefile installs .[bench] (xmltodict, lxml) and a release extension first. Committed snapshot: benchmark_data/benchmark_results.json.

Usage

from xml_iterator.xml_iterator import iter_xml
from xml_iterator.core import xml_to_dict  # xml_iterator.xml_to_dict

# Streaming: records under open wrappers
records = 0
for count, event, value in iter_xml('file.xml'):
    if event == 'end' and value == 'FinInstrm':
        records += 1
        # handle; discard — do not accumulate under open parents
        if records >= 1000:
            break

# Full document dict — modest files / xmltodict parity only
data = xml_to_dict('small.xml')

Also: get_edge_counts(path), opt-in attrs via iter_xml(path, attributes=True). Example: examples/firds_shape_stream.py. Sample event dump: examples/simple.xml + examples/example_xml_iter.py.

Limits: file paths only (no pipes); namespace prefixes stripped; full-file xml_iterator.xml_to_dict is not the multi-GB path.

When to use something else: stdlib ET.iterparse + clear(), bigxml, or xmltodict item_depth callbacks — see landscape doc above.

Develop

make develop          # release extension (default)
make develop-debug    # debug build (~9× slower)
pytest                # after: uv pip install -e ".[test]"

Changelog: CHANGELOG.md.

Release (git tag → CI → PyPI + GitHub)

Already wired: tag v* runs .github/workflows/CI.yml (build/test, then maturin upload using repo secret PYPI_API_TOKEN).

# 1. Bump Cargo.toml version, update CHANGELOG.md, commit, push main
# 2. Annotated tag + push (this publishes wheels to PyPI when CI is green)
git tag -a v0.2.1 -m "v0.2.1: short summary"
git push origin v0.2.1
gh run watch   # optional: wait for CI / Release job

# 3. GitHub Release page (notes only; packages already on PyPI from step 2)
gh release create v0.2.1 --title "v0.2.1" --notes-file CHANGELOG.md
# or click "Draft a release" on the tag in the GitHub UI

PyPI: https://pypi.org/project/xml-iterator/
Releases: https://github.com/cottrell/xml_iterator/releases

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

xml_iterator-0.2.13-cp37-abi3-win_amd64.whl (332.4 kB view details)

Uploaded CPython 3.7+Windows x86-64

xml_iterator-0.2.13-cp37-abi3-manylinux_2_34_x86_64.whl (478.0 kB view details)

Uploaded CPython 3.7+manylinux: glibc 2.34+ x86-64

File details

Details for the file xml_iterator-0.2.13-cp37-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for xml_iterator-0.2.13-cp37-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 b4387d01735cc19f71ab2928e1fa119ddd801da4e1e1e2b2148c367a6e49a8f1
MD5 bcc6d470481b643aa6d881f40fdd6be3
BLAKE2b-256 c1e935f0a50ef65d6499bdedcaefb77c93c38295d348a94e8a1273394d62d11a

See more details on using hashes here.

File details

Details for the file xml_iterator-0.2.13-cp37-abi3-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for xml_iterator-0.2.13-cp37-abi3-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 cde973727a929edb601695b9fde729597d9140d479dd979bfbf3bed2722071e5
MD5 18c5743df924766545990fa668ccf90d
BLAKE2b-256 397cafff35dbe9280b99d493de63b1e69f8113b19de9c70e93d814136b9fc0d9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page