Skip to content
datahacks.dev
XML

What XML Is Good At and Why It Still Shows Up in Your Data

A tested beginner guide to XML: where you will meet it in data work, what it does better than JSON, how it is structured, and the first checks to run.

17 min read
Summarize in AI
Flat illustration of paired angle-bracket tags wrapped around a document, with the XML logo.

You did not choose XML. It was in the spreadsheet you opened, the sitemap your site publishes, the feed a vendor sends every night, the config file of your build tool and the export that arrived with the words "legacy system" attached. XML is thirty years old, and it holds more of the world's structured information than any newer format. If you work with data, you will be asked to read it, and the first hour is the worst, because every tutorial assumes you already know what it is for.

This guide is the orientation I would want: what XML is good at, where it hides, how it is structured, what goes wrong first, and where to go next. The examples are small files I ran through real tools, with the numbers the tools printed.

What XML actually is

XML, the Extensible Markup Language, is a way to write a tree as text. Everything in a document is one of a small set of things.

An element is a named node, written with a start tag and an end tag: <price>12.50</price>. Elements nest, and the nesting is the tree. A document has exactly one outermost element, the root.

An attribute is a name and a value attached to an element's start tag: <price currency="USD">. It is one string, single per name, with no structure of its own.

Text is what is between the tags. Comments, processing instructions and CDATA sections are the rest. And the whole is usually opened by a declaration that names the version and the character encoding.

XML
<?xml version="1.0" encoding="UTF-8"?>
<catalog>
  <book id="b1" lang="en">
    <title>Cache Patterns</title>
    <price currency="USD">12.50</price>
  </book>
</catalog>

That is all there is. The rest is conventions and standards built on top of this one idea: a document is a tree of named, ordered nodes, and any tool that can read the tree can read any XML. The grammar is strict, and a parser reads or refuses a document, which is why the same format has worked for decades.

What XML does well

Compared with JSON, which is the format most people reach for now, XML has strengths that are real and specific.

It handles text with markup. In <p>Hello <b>big</b> world</p> the bold word sits in the middle of a sentence. XML represents that exactly. JSON has no good way to. This is why books, articles, legal texts, manuals and web pages are marked up documents, not records, and why the formats that carry them (DocBook, DITA, EPUB, XHTML) are XML.

Order is data. Children appear in a defined sequence and a parser preserves it. A list of steps, a table of rows or a sentence of words has an order that matters, and XML keeps it without any convention.

It has namespaces. Two vocabularies can share one document without their element names colliding. An Atom feed can carry Dublin Core metadata and a media extension at once, each element tagged with the vocabulary it comes from (how namespaces work).

It has mature validation. A DTD or an XML Schema can state what a document must look like, down to types and enumerations, and a tool checks it (well formed versus valid). Schematron adds business rules. Industries built contracts on that.

It has a toolchain for transformation and query. XPath selects nodes, XSLT turns one XML document into another document or into text, and XQuery queries collections. They are standard and have been stable for twenty years.

It carries comments and processing instructions. A config file can explain itself, and a document can carry directions to the program that reads it.

Its weaknesses are the mirror image. It has no native numbers, booleans, null or arrays, so every value is text until a schema says otherwise. A list of one item is indistinguishable from a single item without the schema. It is verbose: the same 50,000 records were 6.40 MB as XML and 4.38 MB as JSON, and parsed about twice as slowly in Python (XML versus JSON for data exchange has the numbers and what a conversion loses). And it has traps for the careless: namespaces, entities and encodings.

A tour of where it hides

The quickest way to see why XML persists is to open some files. Each of these is a format you probably use already.

Your spreadsheet is a zip of XML

An .xlsx file is a zip archive. I generated a workbook with 2,000 rows and listed it:

Text
[Content_Types].xml                  1167 bytes
_rels/.rels                           587 bytes
xl/_rels/workbook.xml.rels            697 bytes
xl/worksheets/sheet1.xml           243612 bytes   (compressed 30946)
xl/workbook.xml                       550 bytes
xl/sharedStrings.xml                 1611 bytes
xl/styles.xml                         867 bytes
xl/theme/theme1.xml                  6994 bytes
docProps/core.xml                     592 bytes
docProps/app.xml                      784 bytes

Ten parts, every one of them XML. The data is in sheet1.xml, which has a row element per row and a c (cell) element per cell:

XML
<row r="2" spans="1:4"><c r="A2"><v>1</v></c><c r="B2" t="s"><v>4</v></c><c r="C2"><v>1.5</v></c></row>

The attribute t="s" means the value is an index into sharedStrings.xml, which holds each distinct string once (55 unique strings for 2,289 uses). The root element is in the namespace http://schemas.openxmlformats.org/spreadsheetml/2006/main, which is the thing to know before you write an XPath against it. The 2,000 rows made 243,612 bytes of XML that compressed to 30,946 inside the zip.

That structure is the reason you can read a workbook without Excel. At 100,000 rows the sheet was 13.91 MB of XML in a 1.7 MB file. Reading it by streaming the XML with lxml.iterparse and summing a column took 0.64 s, against 1.72 s for openpyxl in read-only mode and 2.12 s for pandas.read_excel, which sits on top of an XML reader. The XML is not a thing to be afraid of. It is the fastest way into the file if you know what to look for, and the same structure holds for Word (.docx) and PowerPoint (.pptx).

Sitemaps, feeds and icons

A sitemap, the file that tells search engines your URLs, is XML in a namespace:

XML
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/</loc><lastmod>2026-01-02</lastmod></url>
</urlset>

An RSS or Atom feed is the same kind of file, with item or entry elements. An SVG image is XML in the SVG namespace, where a circle is <circle cx="32" cy="32" r="30"/> and a shape's style is an attribute. All three were well formed under xmllint, and I read a value out of each with an XPath: the two sitemap URLs, the title First & best of the feed item (note that the file says &amp;, which the parser turns back into an ampersand), and 64 for the icon's width.

SOAP and enterprise integrations

SOAP web services, banking and insurance messages, healthcare documents and customs declarations are XML, defined by schemas that standards bodies publish. A request looks like this:

XML
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:m="urn:example:orders">
  <soap:Body><m:GetOrder><m:Id>7</m:Id></m:GetOrder></soap:Body>
</soap:Envelope>

Two namespaces, one for the envelope and one for the application. You will meet this when you integrate with a system that is older than your company. The contract is a schema file, and the first thing to do is validate your messages against it.

Build and configuration files

Maven's pom.xml, Android layouts and manifests, .NET project files, Spring configuration, Ant builds and macOS property lists are XML. The pom.xml I used declares a default namespace and an xsi:schemaLocation attribute that points to its schema, and its dependencies were readable with an XPath on the local names (artifactId returned app and jackson-databind). Config in XML has comments and a schema, which is why the format lasted there.

Everything else

Scientific data (GPX and KML for tracks and maps, CML for chemistry, SBML for biology), publishing (EPUB, DocBook, JATS for journals), finance (XBRL for company reports, FpML), government and legal data (AKN, XBRL again) and countless exports from software written between 1998 and 2015. If a document has structure that needs to be validated and kept for decades, someone probably chose XML for it.

First checks for a file you have never seen

When an XML file arrives, three checks save most of the pain.

Is it well formed? xmllint --noout file.xml prints nothing if it is, and one error line if it is not. The rules are short (one root, matched tags, quoted attributes, escaped & and <), and the error names the line. The cheat sheet of syntax rules lists 51 cases with their messages, and fixing XML parse errors fast goes through the failures you will see.

Does the encoding match the bytes? The declaration says what the file claims. A file whose bytes are UTF-8 but whose declaration says ISO-8859-1 parses without an error and gives café (the wrong encoding problem). Compare the declaration with what file reports.

What namespace is it in? Look at the root element for xmlns. If there is a default namespace, every unprefixed element is in it, and a path like //entry finds nothing until you map a prefix. This single fact explains most "my XPath returns nothing" questions (namespaces).

After that, look at the structure. The XML formatter shows a document indented, and xmllint --format file.xml does the same on the command line. Find the repeating element (row, item, order): that is your record.

Getting data out

There are three levels of tool, and you will use all of them.

Query it where it is. XPath selects nodes: //item[price>10]/title. In the command line xmllint --xpath and in code lxml's .xpath() do it. Ten expressions cover most needs (XPath queries that cover most real needs), and the XPath tester lets you try one on a sample.

Read it in code. Python's standard library has xml.etree.ElementTree, and lxml is the faster and fuller library (parsing and writing XML in Python). For large files, stream with iterparse and clear nodes as you go, which handled a 14 MB sheet in well under a second (parsing large XML files with iterparse). For untrusted input, use defusedxml (safe XML parsing in Python).

Load it into a database. PostgreSQL has an xml type and xmltable, SQL Server has nodes() and value(), Oracle has XMLTABLE, and MySQL has ExtractValue and LOAD XML (storing and querying XML inside a database). When the destination is tables, the pattern is to find the repeating element and turn each into a row (turning XML feeds into relational tables).

The common first mistakes

A short list of what bites newcomers, each covered in a spoke.

  1. **A raw & or < in text** from string concatenation. Use a serializer (escaping special characters).

  2. A path that finds nothing because of a default namespace.

  3. HTML entities like &nbsp; in XML, which are undefined.

  4. Treating indentation as data. Whitespace between elements is text in the tree (why whitespace sometimes matters).

  5. Trusting the encoding declaration.

  6. Parsing untrusted XML with a parser that resolves entities. A DTD can read files or expand a few hundred bytes into gigabytes (what a DTD is). Reject documents that carry a DOCTYPE unless you need one.

  7. Assuming a list. An element that repeats is a list; one that appears once is not, unless you handle it.

Choosing XML, or not

If you are designing a new format for records between systems you control, use JSON. It is smaller, faster and simpler. If your content is documents with inline structure, a long-lived archive with strict validation, a vocabulary shared by many parties, or an existing standard, use XML. When a counterparty decides for you, learn to read it well; it is easier than its reputation.

A learning path through this cluster

The articles on this site that follow the guide fall into five groups. The basics (this guide plus the nine that follow it) explain the syntax, validity, attributes, namespaces, escaping, whitespace, encodings, DTDs and oddities. Debugging (fixing XML parse errors fast) works through each error message and its cause. XPath (XPath queries that cover most real needs) covers selecting nodes. Python (parsing and writing XML in Python) shows the libraries. And the database and pipeline groups show how to get XML into tables.

The primary references are the XML 1.0 recommendation, which is short and readable, and the Namespaces in XML recommendation. The libxml2 tools used above are documented at the GNOME libxml2 wiki.

A short history, and why the format stayed

XML came out of SGML, the heavyweight markup language used for technical publishing, and the W3C published XML 1.0 as a recommendation in 1998. The goal was a simpler SGML that could travel on the web: strict enough for programs to parse with no guessing, flexible enough to define any vocabulary. Within a few years it was everywhere, as the format for configuration, for web services (SOAP and its relatives), for office documents and for data exchange between companies.

Then JSON arrived and took the part of the job that was always awkward in XML: sending records between a browser and a server. For that, JSON is smaller, simpler and closer to the data structures programs already use. XML did not go away, because it was never only a record format. The standards written in it were expensive to write and are expensive to replace: tax filings, healthcare messages, publishing pipelines, mapping data, accounting formats. The tools (parsers, validators, XSLT engines) are stable, free and in every language. A format that works and is already specified in your industry is hard to dislodge.

So the practical position is: you will not often choose XML for new work, and you will often be handed it.

Reading a spreadsheet's XML yourself

The workbook example is worth doing once with your own hands, because it shows the whole workflow in a few lines. A sheet is a stream of row elements. Streaming them with iterparse, and clearing each row once it has been handled, keeps memory flat however many rows there are:

Python
M = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}"
with zipfile.ZipFile("big.xlsx").open("xl/worksheets/sheet1.xml") as f:
    for event, el in etree.iterparse(f, tag=M + "row"):
        for c in el:
            if c.get("r", "").startswith("C"):
                total += float(c.find(M + "v").text)
        el.clear()
        while el.getprevious() is not None:
            del el.getparent()[0]

Three things in it are the lessons of this guide in miniature. The tag is written with the namespace in curly braces, because the elements are in the spreadsheet namespace. The loop reads cells by their r attribute (C2, C3) because the document is a tree of named things and not a grid. And the clearing at the end keeps the tree from growing: without it the same loop would hold the whole sheet in memory. For my 100,000 row file the sum of the third column came out as 5,000,000.0 in 0.64 s.

Note that strings in a workbook may be stored inline or in sharedStrings.xml, depending on the program that wrote the file, and a cell with t="s" holds an index, not the text. If you are reading real files from Excel and not generated ones, reach for a library (openpyxl, pandas) and keep the raw approach for when the library is too slow or too memory-hungry.

What a first project looks like

The usual first task is turning an XML feed into a table. With the sitemap and the RSS feed from above, it took a few lines:

Python
ns = {"s": "http://www.sitemaps.org/schemas/sitemap/0.9"}
rows = [{"loc": u.findtext("s:loc", namespaces=ns),
         "lastmod": u.findtext("s:lastmod", namespaces=ns),
         "changefreq": u.findtext("s:changefreq", namespaces=ns)}
        for u in tree.iterfind("s:url", ns)]
pd.DataFrame(rows)
Text
                      loc    lastmod changefreq
     https://example.com/ 2026-01-02        NaN
https://example.com/about        NaN    monthly

Two points show up immediately. Optional elements become missing values (lastmod is absent on the second URL, so findtext returned None), which is the XML way of saying "not provided". And everything is text: the RSS publication date Fri, 02 Jan 2026 03:04:05 GMT needed an explicit format string to become a timestamp, 2026-01-02 03:04:05+00:00. Pandas can do the whole thing in one call, pd.read_xml("sitemap.xml", xpath=".//s:url", namespaces=ns), which gave the same table with the priority column included. Without the namespace mapping it raised ValueError: xpath does not return any nodes: the default-namespace trap in its most common form.

That is the whole shape of the work. Find the repeating element, name its namespace, take the fields you want, convert the types, and decide what a missing element means.

What it costs

Numbers from the other articles in this cluster give a feel for the practical costs. The same 50,000 orders took 3.83 MB written with attributes, 5.88 MB with child elements and 7.43 MB indented, and all of them compressed to between 0.25 and 0.28 MB, so size matters in memory and on disk but not on the wire (attributes versus child elements). Parsing them took 0.09 to 0.32 s depending on the parser and the layout. The parse of a 6.4 MB document took 0.109 s in lxml and 0.174 s in ElementTree. Neither is slow, and both are slower than the equivalent JSON.

The costs that hurt are not speed. They are the hours spent on a namespace, an encoding or an entity. The cheat sheet, the encoding article and the entity article exist for those.

Security in one page

XML parsers have a history of vulnerabilities, and the cause is a feature: the DTD. A document can declare entities, and an entity can point at a file (file:///etc/hostname) or a URL, or expand a few hundred bytes into hundreds of thousands of characters. Current ElementTree and lxml refuse to read external files by default and cap entity expansion: an eight-level expansion was rejected by both. A parser with entity resolution switched on read the file's contents, and defusedxml rejected every document that declared an entity at all.

The rule for anything from outside your own systems is to use a parser configured for untrusted input, to reject documents with a DOCTYPE, and to limit size and depth before parsing, as for any format (what a DTD is and why you rarely need one, safe XML parsing in Python). The same attitude toward untrusted input that applies to JSON (JSON security mistakes in apps and pipelines) applies here, and XML adds the entity features to the list.

The vocabulary you will keep running into

A handful of terms recur in every XML conversation, and knowing them makes documentation readable.

A node is any item in the tree: an element, an attribute, a piece of text, a comment. The root is the single outermost element. Well formed means it obeys the syntax. Valid means it also follows a schema.

A namespace is a URI that scopes names, and a prefix is a local alias for it. The URI is the identity and the prefix is a convenience. A default namespace is one declared without a prefix, which applies to unprefixed elements.

A DTD is the old schema language, and XSD (XML Schema) the newer one, written in XML itself. Schematron states rules as XPath assertions. RELAX NG is a third, less common alternative.

XPath is the language for selecting nodes. XSLT transforms a document by applying templates, and XQuery queries collections the way SQL queries tables.

DOM is the model where a parser loads the whole tree into memory and you walk it. SAX and StAX are streaming models that report events as the parser reads, so memory stays small. ElementTree and lxml are DOM-like with a streaming option (iterparse) that gives you the middle path: events for each element, with the subtree available while you work on it.

An entity is a named piece of text, and a character reference (&#169;) is a numeric way to write a character. CDATA is a section where markup characters are literal.

Finally, canonical XML (c14n) is a defined way to write a document so that two logically equal documents have identical bytes. It is what digital signatures use, and what you would use to compare two documents byte for byte if layout differences should not count.

None of these needs mastering before you start. They are the words that explain an error message, and now you can look each one up.

What to learn next, in order of payoff

If I had one week to become useful with XML, I would spend the first day on XPath, because selecting the right nodes is most of the job. The second day goes to a library in the language I use, reading, writing and streaming. The third day is for namespaces and encodings, which are where files go wrong. The fourth is validation: running a file against a supplied XSD with xmllint and reading the errors. The fifth day is for getting data into the place it is needed, tables or JSON, with a plan for missing elements and repeated ones. After that, XSLT is worth a look if you convert documents often, and Schematron if you own a format. Everything on that list has an article here, in this cluster, with the commands run and the output shown.

How to approach the cluster

If you are new, read in this order: the syntax cheat sheet, well-formed versus valid, namespaces, escaping, and the encoding article. Those five remove most of the first-day problems. Then take whichever of debugging, XPath or Python matches what you are doing, and come back to the database articles when you need to load the data into tables. Each article is one idea with one or two experiments you can repeat; every command above ran as shown.

When XML is not the format you need

You are designing a record exchange between your own services. JSON is smaller, faster to parse and has the types you want. Do not take on namespaces and schemas without a reason.

The data is tabular. A CSV or a Parquet file is the right shape for rows and columns. XML would add size and a mapping step. If you receive XML that is really a table, convert it once and work with the table.

You need human-edited configuration. YAML and TOML are friendlier to type than XML for small configs, though XML offers comments and validation too.

Speed and size are the constraint. Binary formats and JSON Lines beat XML for high-volume data. Keep XML for the cases where its features are the point.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.