Knowledge Base

How to Parse XML with Lxml

A hands-on guide to parsing XML documents with lxml, covering loading, tree navigation, attributes, namespaces, and XPath queries for clean data extraction.

XML is everywhere in data work, from sitemaps and RSS feeds to API responses and configuration files. Python's lxml library is one of the fastest and most reliable ways to read that structure, thanks to its C-backed parser and full XPath support.

This guide shows how to parse XML with lxml step by step: loading a document, walking the element tree, reading attributes, dealing with namespaces, and running targeted queries to pull out exactly what you need.

Quick answer

Once you can load and query XML with lxml, the next concerns are scale and safety: stream large files with iterparse instead of loading them whole, disable external-entity resolution to block XXE attacks, and serialise your changes back out with correct encoding and a declaration. lxml also lets you build and transform documents, not just read them.

Key takeaways

  • <code>etree.iterparse</code> processes huge XML in constant memory if you call <code>clear()</code> on finished elements
  • A default <code>XMLParser</code> can resolve external entities, so set <code>resolve_entities=False</code> and <code>no_network=True</code> for untrusted input
  • Serialise with <code>etree.tostring(root, xml_declaration=True, encoding="UTF-8")</code> to preserve the declaration and charset
  • lxml can build trees from scratch with <code>etree.Element</code> and <code>SubElement</code>, useful for generating feeds and sitemaps
  • XSLT transformations run natively via <code>etree.XSLT</code>, turning one XML shape into another without manual looping
  • <code>findall</code> uses limited ElementPath syntax, while <code>xpath()</code> gives the full language including functions and axes

Installing and Importing lxml

The XML tools live in the etree module. Install the package and import it before you start.

pip install lxml

from lxml import etree

The etree module gives you both parsing functions and the element classes you will use to traverse the result.

Parsing from a String or a File

If your XML is already in memory as a string, use fromstring(). If it lives on disk, use parse(), which returns an ElementTree wrapper rather than the root element directly.

xml_data = """
<catalog>
  <proxy type="residential">
    <country>Germany</country>
    <rotating>true</rotating>
  </proxy>
  <proxy type="datacenter">
    <country>Netherlands</country>
    <rotating>false</rotating>
  </proxy>
</catalog>
"""

root = etree.fromstring(xml_data)

# From a file instead:
# tree = etree.parse('catalog.xml')
# root = tree.getroot()

Either way you end up with a root Element that you can iterate and query.

Navigating the Element Tree

Each element is iterable over its direct children, and you can read tags and text easily.

for proxy in root:
    country = proxy.find('country').text
    rotating = proxy.find('rotating').text
    print(proxy.tag, country, rotating)

find() returns the first matching child, while findall() returns every match. Use iter() to walk descendants at any depth.

Reading Attributes

Attributes are exposed through the .attrib dictionary or the .get() method.

for proxy in root.findall('proxy'):
    print(proxy.get('type'))     # 'residential', 'datacenter'

Using .get() is safer than indexing .attrib directly because it returns None when the attribute is missing instead of raising an error.

Querying with XPath

XPath is where lxml shines. Instead of looping manually, describe what you want and let the engine find it.

# All country names
countries = root.xpath('//country/text()')

# Only residential proxy countries
res = root.xpath('//proxy[@type="residential"]/country/text()')

Predicates like [@type="residential"] filter by attribute, and functions such as contains() and starts-with() let you match partial values.

Handling Namespaces

Real-world XML, especially feeds and standards-based formats, often declares namespaces. You must register a prefix map to query namespaced elements.

ns = {'atom': 'http://www.w3.org/2005/Atom'}
links = root.xpath('//atom:link/@href', namespaces=ns)

Skipping the namespace map is the most common reason an XPath query silently returns nothing on otherwise valid XML.

Error Handling and Recovery

Malformed documents raise an XMLSyntaxError. For messy or partially broken sources, a forgiving parser can help.

parser = etree.XMLParser(recover=True)
root = etree.fromstring(broken_xml, parser=parser)
  • Use recover=True to skip past minor syntax problems.
  • Wrap parsing in try / except etree.XMLSyntaxError to log and continue on bad input.
  • Validate against a schema with etree.XMLSchema when correctness is critical.

Putting It Together for Scraping

When you fetch XML endpoints at scale, such as multilingual sitemaps or regional feeds, pairing lxml parsing with rotating proxies keeps requests flowing without tripping rate limits, while the parsing logic above turns each response into clean, structured records.

Comparison snapshot

A quick value-first shortlist — Cheapest Proxies leads as the featured pick. Qualitative labels only; confirm exact plans before buying.

ProviderBest forProfileValue
Bright DataEnterprises needing huge pools and compliance controlsEnterprise FocusedPremium
OxylabsLarge-scale scraping and data APIsEnterprise FocusedPremium
Smartproxy (Decodo)Newcomers who want an easy dashboardBeginner FriendlyGood
SOAXPrecise city and carrier targetingAutomation FriendlyGood

Parsing Untrusted XML Safely

XML parsing has a well-known security pitfall: external entity expansion, known as XXE, which can read local files or trigger network calls when you parse a hostile document. The base guide shows how to recover from malformed input, but recovery is not the same as safety. For any XML you did not author, harden the parser explicitly.

parser = etree.XMLParser(
    resolve_entities=False,
    no_network=True,
    huge_tree=False,
)
root = etree.fromstring(untrusted_bytes, parser=parser)

Disabling entity resolution stops the classic billion-laughs and file-disclosure attacks, while no_network=True prevents the parser from fetching remote DTDs. Leaving huge_tree off keeps lxml's built-in limits in place, which guard against deliberately oversized documents designed to exhaust memory.

Streaming Large Documents Without the Memory Hit

A multi-gigabyte sitemap or export will not fit comfortably in a single in-memory tree. The event-driven iterparse approach reads one element at a time and lets you discard it once processed.

for event, elem in etree.iterparse('export.xml', tag='record'):
    handle(elem.findtext('id'), elem.findtext('name'))
    elem.clear()
    while elem.getprevious() is not None:
        del elem.getparent()[0]

The clear() call empties the element you just used, and the inner loop deletes already-processed siblings so the root does not slowly fill with emptied shells. This is the difference between a job that completes and one that is killed by the operating system for using too much memory.

Building and Modifying Trees, Not Just Reading

Parsing is half the story; lxml is equally good at constructing XML. You can assemble a document programmatically, set attributes and text, then serialise it.

root = etree.Element('catalog')
proxy = etree.SubElement(root, 'proxy', type='residential')
etree.SubElement(proxy, 'country').text = 'Germany'

out = etree.tostring(root, pretty_print=True,
                     xml_declaration=True, encoding='UTF-8')

Always pass an explicit encoding when serialising. Without it lxml returns a Python str and omits the declaration, which can break consumers that rely on the charset line. pretty_print=True is fine for human-readable output but adds whitespace, so avoid it when byte-for-byte fidelity or signature validation matters.

Transforming XML with XSLT

When you need to reshape one XML vocabulary into another, native XSLT often beats hand-written traversal. lxml compiles a stylesheet once and applies it to many documents.

xslt = etree.XSLT(etree.parse('transform.xsl'))
result = xslt(root)

This is ideal for converting vendor feeds into your own schema, or flattening nested records into a tabular form, and it keeps transformation logic declarative and in one file rather than scattered across Python loops.

Pros and cons to weigh

Strengths

  • lxml covers the full lifecycle: parse, query, build, transform and serialise in one fast library
  • Event-driven parsing handles arbitrarily large files in constant memory
  • Parser hardening options give real protection against XXE and entity-expansion attacks
  • Native XSLT support replaces brittle manual conversion code with declarative stylesheets
  • Full XPath, including axes and functions, is available where ElementTree's path syntax falls short

Trade-offs

  • Default parser settings are convenient but unsafe for untrusted input until you tighten them
  • Serialisation silently drops the XML declaration unless you pass an explicit encoding
  • <code>pretty_print</code> alters byte content, which can invalidate signatures or hashes
  • The C dependency means installation can fail on minimal systems lacking build tools or wheels

Common mistakes to avoid

  • Parsing third-party XML with default settings, leaving the door open to XXE
  • Loading enormous files into a full tree instead of streaming with <code>iterparse</code> and <code>clear()</code>
  • Forgetting <code>encoding</code> on <code>tostring()</code> and shipping output with no declaration
  • Reaching for <code>findall</code> for a query that needs full XPath functions, then fighting its limited syntax

Before-you-buy checklist

  • Confirm whether the source is trusted; if not, harden the parser before reading it
  • Estimate file size and switch to streaming above a few hundred megabytes
  • Register namespace prefixes for any namespaced document you intend to query
  • Decide on an explicit output encoding before serialising
  • Skip <code>pretty_print</code> when exact bytes are required for hashing or signing
  • Keep XSLT in mind for repeated, rule-based reshaping instead of ad hoc loops
$

How to get the best value

Right-size the plan

Start on the smallest sensible tier and scale only what proves itself on your real targets.

Type before brand

Pick the proxy type the task needs first — it drives both success rate and cost more than the logo.

Read the fine print

Check traffic limits, rotation rules and what happens on overage before you commit.

Lead with value

Our featured value pick, Cheapest Proxies, is a sensible starting point for affordable comparison.

📖

Key terms explained

XXE
an XML external entity attack that abuses entity resolution to read files or make network requests, mitigated by disabling entity resolution
iterparse
an incremental, event-based parser that yields elements as they are read so you can process and discard them
ElementTree path
the simplified expression syntax used by <code>find</code> and <code>findall</code>, a subset of full XPath
XSLT
a declarative language for transforming one XML document into another, executed natively by lxml
XML declaration
the leading line specifying version and encoding, included on serialisation only when you pass an explicit encoding

Why compare before buying?

The parser handles the data, but the requests that fetch it depend on your proxy setup, and providers differ a lot on price, reliability and geographic coverage. Comparing them on value first means you do not overpay for capacity you will not use. For tighter budgets, Cheapest Proxies is our featured value pick worth weighing against the alternatives.

How we compare

Compare Proxy Zone weighs providers on value, fit and reliability using qualitative judgement — never invented prices, speeds or uptime figures. See our review methodology, or email info@compareproxyzone.com with a correction.

?

Frequently asked questions

What is the difference between fromstring() and parse() in lxml?

fromstring() takes XML text and returns the root element, while parse() reads a file or file-like object and returns an ElementTree; call getroot() on the latter.

Why does my XPath return nothing on valid XML?

The document almost certainly uses namespaces. Pass a namespaces={'prefix': 'uri'} map to xpath() and prefix your element names accordingly.

How do I read an attribute value safely?

Use element.get('name'), which returns None if the attribute is absent, rather than indexing element.attrib['name'] which raises a KeyError.

Can lxml parse broken or malformed XML?

Yes, create a parser with etree.XMLParser(recover=True) and pass it in; lxml will skip minor errors and recover as much of the tree as possible.

What is the difference between find() and findall()?

find() returns the first matching child element or None, while findall() returns a list of all matching elements at that level.

How do I validate XML against a schema with lxml?

Load the XSD with etree.XMLSchema(etree.parse('schema.xsd')), then call schema.validate(tree) or assertValid() on your parsed document.

Is lxml faster than the built-in ElementTree?

For most workloads lxml is faster and offers full XPath plus better namespace handling, though the standard library's xml.etree is fine for small, simple documents.

Compare on value, then decide

For affordable proxies across the main types, our featured value pick is Cheapest Proxies — a strong budget-friendly option worth considering. Check the exact plan before ordering.