Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Data Formats (JSON, YAML, TOML, XML, CSV)  Module 7 – XML 1.0: syntax, namespaces, parsing, security, schemas and signatures

XML syntax and well-formedness

Write XML every parser accepts: one root, matching tags, quoted attributes and escaped text, plus CDATA, comments and how to read an error position.

  • Beginner
  • 20 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7, Ruby 4.0.7 and sql.js 1.14.2
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Write a well-formed document with one root, nested tags and quoted, unique attributes
  • Find the mistake behind a parser's fatal error from its line and column
  • Distinguish a well-formed document from a valid one
  • Encode text and attribute values safely, and use character references, CDATA and comments correctly
  • Choose between an element and an attribute, and keep mixed content in order

Before you start

What you’ll learn · Before you start

What you will learn

  • Write a well-formed document with one root, nested tags and quoted, unique attributes
  • Find the mistake behind a parser's fatal error from its line and column
  • Distinguish a well-formed document from a valid one
  • Encode text and attribute values safely, and use character references, CDATA and comments correctly
  • Choose between an element and an attribute, and keep mixed content in order

Before you start

On this page

One bare ampersand, in an item called Fish & Chips, is enough to make a parser reject a whole product feed. XML has no repair mode: the specification calls a broken rule a fatal error, after which a processor “MUST NOT continue normal processing”, so every other item of the feed is lost with the one that broke the rule.

The rules that prevent this are few and exact. You will write documents that every parser accepts, turn a parser’s line and column into the mistake behind them, escape text and attribute values correctly, see what CDATA sections, comments and processing instructions are for, and decide when a value belongs in an element and when in an attribute.

Six rules make a document well-formed

Here is one order of a bookshop as XML. It has elements (<branch>Pune</branch>), attributes (sku="BK-101"), an empty element written as one tag (<note/>), a comment and, on the first line, the XML declaration:

order.xml: one well-formed order XML · order/order.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- One bookshop order from the Pune branch -->
<order id="BK-10042" placed="2026-03-14T18:45:00+05:30">
  <branch>Pune</branch>
  <customer type="member">Asha Rao</customer>
  <items>
    <item sku="BK-101" qty="2">Data Formats in Practice</item>
    <item sku="BK-205" qty="1">Fish &amp; Chips: A Kochi Cookbook</item>
  </items>
  <amount currency="INR">379.00</amount>
  <note/>
</order>

A document like this is well-formed: Following the syntax rules of XML 1.0: one root element, tags that match and nest, quoted attribute values with no name repeated, and < and & escaped. A parser refuses any document that is not well-formed. when it keeps six rules of the XML 1.0 specification:

  1. There is exactly one root element: The one element of an XML document that contains all the others, also called the document element. Comments and processing instructions may stand outside it, but no other element may., here order, and it contains every other element. Comments and processing instructions may sit before and after it, but no second element.
  2. Every start tag has an end tag with the same name, or the element is written as an One tag that opens and closes an element at once, written with a slash just before its closing bracket. It means the same as a start tag followed at once by its end tag.. Names are case-sensitive: <Item> is not closed by </item>.
  3. Elements nest: the element opened last is closed first, so <b><i>sale</b></i> is not allowed.
  4. Every attribute value is in quotes, single or double, and an attribute name appears at most once in a tag.
  5. The characters < and & never appear on their own in text or in attribute values; the next sections show how to write them.
  6. The XML declaration, if there is one, is the very first thing in the file, with not even a blank line before it.

Names have rules of their own. A name starts with a letter or an underscore and may then contain digits, hyphens and dots, so 1st-floor is not a name. Names that start with xml, in any mix of capitals, are reserved for the standards. The colon is allowed, but the specification asks authors to keep it for namespaces, the subject of the next lesson.

The program below puts ten small documents, each breaking one rule, through Python’s ElementTree and prints what it reported. Press Edit, mend one of the documents and run it again: its line then says accepted.

Ten broken documents and what ElementTree reports Python · broken/broken.py
"""Ten small documents, each breaking one rule of well-formed XML, through Python's ElementTree."""
import xml.etree.ElementTree as ET

BROKEN = [
    ("two root elements", "<order/><order/>"),
    ("end tag does not match", "<item>Pens</itm>"),
    ("tags overlap", "<b><i>sale</b></i>"),
    ("names differ in case", "<Item>Pens</item>"),
    ("attribute not quoted", "<item qty=2/>"),
    ("attribute written twice", '<item qty="2" qty="3"/>'),
    ("bare ampersand in text", "<name>Fish & Chips</name>"),
    ("< in an attribute value", '<rule test="a<b"/>'),
    ("name starts with a digit", "<1st-floor/>"),
    ("declaration not first", '\n<?xml version="1.0"?><order/>'),
]

print(f"{'rule broken':<26} {'line':>4} {'col':>4}  message")
for rule, text in BROKEN:
    try:
        ET.fromstring(text)
        print(f"{rule:<26} accepted")
    except ET.ParseError as err:
        line, column = err.position
        message = str(err).split(":")[0]
        print(f"{rule:<26} {line:>4} {column:>4}  {message}")

Output

rule broken                line  col  message
two root elements             1    8  junk after document element
end tag does not match        1   12  mismatched tag
tags overlap                  1   12  mismatched tag
names differ in case          1   12  mismatched tag
attribute not quoted          1   10  not well-formed (invalid token)
attribute written twice       1   14  duplicate attribute
bare ampersand in text        1   12  not well-formed (invalid token)
< in an attribute value       1   13  not well-formed (invalid token)
name starts with a digit      1    1  not well-formed (invalid token)
declaration not first         2    0  XML or text declaration not at start of entity

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 broken.py

Each document produced a parse error, and the messages are brief: three different mistakes, tags that overlap, names that differ in case and an end tag spelt wrongly, all read mismatched tag. The message names the kind of error; the position says where to look.

Checkpoint

Check: The rules of well-formed XML

  1. Question 1 of 2 Which of these documents is well-formed?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: <!-- one order --><order/>

    A well-formed document has one root element that contains every other element. Comments and processing instructions may stand outside it; a second element may not.

  2. Question 2 of 2 ElementTree reported mismatched tag at line 1, column 12 for this text. What is the smallest correct fix?

    Read the code, then choose one answer.

    <b><i>sale</b></i>
    Show the answer: Question 2 of 2

    Answer: <b><i>sale</i></b>: close i, the element opened last, first

    Column 12 is the name in </b>, which arrived while i was still open. Closing the inner element first is the only edit that keeps both elements around the word.

A parser stops at its first error and reports where it noticed it

A parser reads the text from the start and keeps a list of the elements that are still open. Every end tag must match the element opened last, and the first time one does not, the parser stops. It cannot know where the real mistake was, only where the text stopped making sense. Step through a document whose item on line 2 is never closed:

Interactive · message by message A parser checks every end tag against its open elements Step through the four lines of the never-closed item. Watch the list of open elements, and where the parser finally stops.
A parser checks every end tag against its open elements, step 6 of 6. Messages so far: The document to The parser: <items>; The document to The parser: <item>Pens; The document to The parser: <item>Pads; The document to The parser: </item>; The document to The parser: </items>.The documentfour lines of textThe parserreads tags in orderOpen elementsnone left open<items><item>Pens<item>Pads</item></items>

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Line 1 opens items. The parser reads the start tag <items> and adds items to its list of open elements. The element stays open until an end tag with the same name arrives.
  2. Line 2 opens an item. The start tag <item> opens a second element inside the first, and its text Pens belongs to it. The end tag that should follow Pens is missing, but nothing is wrong yet.
  3. Line 3 opens another. A second <item> opens inside the first one. XML does not know what an item may contain, so an item inside an item is still well-formed.
  4. Line 3 closes one. The end tag </item> matches the element opened last, the inner item, so the parser closes it. Every end tag must match the most recently opened element that is still open.
  5. Line 4 does not match. The end tag </items> arrives while the outer item is still open. That is a fatal error, mismatched tag, and ElementTree reports it at line 4, column 2: two lines after the real mistake.
  6. The fix. Add </item> after Pens on line 2. Then every end tag matches the element opened last, the list of open elements is empty at the end, and the document is well-formed.

The parser accepted lines 2 and 3, because an item inside an item breaks no rule of XML, and stopped on line 4. The next program prints the reported line of that document, of a bare ampersand and of the first document with its last line deleted, each with a caret under the reported column:

Where ElementTree stopped, shown with a caret Python · position/where.py
"""Show where ElementTree stopped: the reported line, with a caret under the reported column."""
import xml.etree.ElementTree as ET

DOCS = {
    "an item that is never closed": "<items>\n  <item>Pens\n  <item>Pads</item>\n</items>\n",
    "a bare ampersand": "<menu>\n  <name>Fish & Chips</name>\n</menu>\n",
    "the same item, </items> deleted": "<items>\n  <item>Pens\n  <item>Pads</item>\n",
}

for title, text in DOCS.items():
    try:
        ET.fromstring(text)
    except ET.ParseError as err:
        line, column = err.position
        print(f"{title}: {str(err).split(':')[0]}")
        print(f"  reported at line {line}, column {column} (column {column + 1} when counted from 1)")
        lines = text.splitlines()
        if line > len(lines):
            lines.append("")  # the parser reached the end of the text, after the last line break
        for number, source in enumerate(lines, start=1):
            print(f"  {number} | {source}")
            if number == line:
                print("    | " + " " * column + "^")
        print()

Output

an item that is never closed: mismatched tag
  reported at line 4, column 2 (column 3 when counted from 1)
  1 | <items>
  2 |   <item>Pens
  3 |   <item>Pads</item>
  4 | </items>
    |   ^

a bare ampersand: not well-formed (invalid token)
  reported at line 2, column 14 (column 15 when counted from 1)
  1 | <menu>
  2 |   <name>Fish & Chips</name>
    |               ^
  3 | </menu>

the same item, </items> deleted: no element found
  reported at line 4, column 0 (column 1 when counted from 1)
  1 | <items>
  2 |   <item>Pens
  3 |   <item>Pads</item>
  4 | 
    | ^

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 where.py

Two habits follow from this run. First, read upwards from the reported line: a missing end tag is reported where the first end tag that does not match arrives, two lines later here and possibly hundreds in a long file. Second, check how your tool counts. ElementTree counts lines from 1 and columns from 0, and it points at the first character it could not accept: for the bare ampersand that is the space after &, because an ampersand must start a reference such as &amp;. An editor that counts columns from 1 shows the same place one column further on.

Fixing the line the error names

Wrong: edit line 4 until the error goes away, for example by deleting </items>. That turns a missing end tag into a document that ends early, and the parser now says no element found at the end of the text, as the third run shows. Right: read upwards from line 4 to the element that is still open, and close it where it ends, after Pens.

XML Formatter Paste a document to check it: errors come with their line and column, counted from 1, and the tag that is still open.

Checkpoint

Check: Fatal errors and their positions

  1. Question 1 of 2 ElementTree stops with mismatched tag at line 40 of a file, at </order>, and line 40 looks right. Where is the mistake most likely to be?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Above line 40: an element opened earlier inside the order was never closed

    Read upwards from the reported line to the element that is still open, and close it where it ends.

  2. Question 2 of 2 ElementTree reported line 4, column 2 for the </items> of the never-closed item. Which column does an editor that counts from 1 show for the same character?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 3 column

    ElementTree counts columns from 0, so its column 2 is the third character of the line, the i of items. A tool that counts from 1, such as an editor, calls it column 3.

Well-formed is not the same as valid

A document can keep every rule of XML and still be wrong for the program that reads it. The three orders below break the shop’s rules, a quantity written as a word, an order without an amount and an element nobody defined, and ElementTree accepts all of them:

Three wrong orders that are well-formed Python · valid/wellformed_not_valid.py
"""Three orders that break the shop's rules but none of XML's: the parser accepts every one."""
import xml.etree.ElementTree as ET

ORDERS = {
    "quantity written as a word": '<order id="BK-10043"><item sku="BK-101" qty="two"/></order>',
    "amount missing": '<order id="BK-10044"><branch>Pune</branch></order>',
    "element the shop never defined": '<order id="BK-10045"><discount>100%</discount></order>',
}

for problem, text in ORDERS.items():
    root = ET.fromstring(text)
    print(f"{problem}: parsed, root <{root.tag}> with {[child.tag for child in root]}")

Output

quantity written as a word: parsed, root <order> with ['item']
amount missing: parsed, root <order> with ['branch']
element the shop never defined: parsed, root <order> with ['discount']

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 wellformed_not_valid.py

Well-formed means the text follows XML’s own syntax. valid: Of an XML document: well-formed and also following a schema, such as a DTD or an XSD, that says which elements and attributes the document has, in which order and of which types. means it also follows a schema: A set of rules that says which values a document may hold, such as which fields it needs and the type of each. JSON Schema and XML Schema (XSD) write those rules as documents of their own. More in “Choosing a data format: JSON, YAML, TOML, XML, CSV”, the rules for one kind of document: which elements and attributes an order has, in which order, and of which types. XML 1.0 itself defines validity against a DTD, the document type definition that a document type declaration contains or points to. Schemas are also written in XML Schema (XSD) or RELAX NG, and a later lesson of this module validates with XSD. Step through the checks a document passes on its way into a program:

Interactive · stage by stage From bytes to data you can trust Step through the checks an order passes on its way into a program. Watch which check catches which mistake.
From bytes to data you can trust, step 6 of 6. Showing Bytes arrive, Decode the characters, Well-formed? (all three orders pass), Valid? (refuses all three orders), Your own checks, Data you can use. In focus: Data you can use.Bytes arrivea file or a request bodyDecode the charactersUTF-8 unless declaredWell-formed?all three orders passValid?refuses all three ordersYour own checksrules the schema leaves outData you can usepassed every check

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Decode. The bytes become characters first: UTF-8, unless a byte-order mark or the XML declaration names another encoding. A byte that is not valid in that encoding is a fatal error, reported where the parser meets it.
  2. Well-formed. The parser checks the six rules of this lesson: one root, matching tags, quoted attributes and escaped text. A bare ampersand stops everything here, with a fatal error.
  3. Three orders pass. The three orders of the example all pass this check: qty="two", a missing amount and an unknown discount element break no rule of XML.
  4. Valid. A schema states which elements and attributes an order has and the type of each. It would refuse qty="two" when qty must be a whole number, the order without an amount, and the discount element nobody defined.
  5. Your checks. Some rules are beyond a schema of elements and types, such as an amount that must match the sum of the items, unless an XSD 1.1 assertion states them. Your program checks those last, on data it already knows is well-formed and valid.
  6. Use the data. Only now does the order reach the code that stores or ships it. Each check runs on input that passed the ones before it.

Each check catches what the earlier ones cannot see, and none replaces another. A parser never refuses qty="two", and a schema that lists elements and their types, as a DTD or an XSD 1.0 schema does, cannot check that an amount equals the sum of the items. XSD 1.1 can state such a rule as an assertion; without one, the rule is your code’s job, on data that passed both checks.

Key idea

Well-formed is a property of the text, checked by every parser; valid is a property of a document against a schema you choose, checked only when you ask for it.

Checkpoint

Check: Well-formed and valid

  1. One question An order arrives as <item sku="BK-101" qty="two"/>, and qty must be a whole number. Which check refuses it?

    Choose one answer.

    Show the answer: One question

    Answer: Validation against the order's schema

    Well-formedness is about syntax only. A value of the wrong type passes every parser and is caught by a schema or by the program's own checks.

Escape five characters, and only where they need it

XML reserves < for tags and & for references, so text that contains them must spell them differently. Five entity reference: A name between & and ; that stands for text, such as &lt; for < and &amp; for &. XML predefines five of them; any other must be declared in the document's DTD. are predefined, and every parser knows them without being told:

Character Write it as Where it must be escaped
< &lt; always, in text and in attribute values
& &amp; always, in text and in attribute values
> &gt; only in the sequence ]]> in text; elsewhere it may stay
" &quot; inside an attribute value in double quotes
' &apos; inside an attribute value in single quotes

Any other character can be written as a character reference: A character written by its Unicode code point between &# and ;, in decimal or in hexadecimal after &#x, so &#8377; and &#x20B9; are both the rupee sign. A parser replaces it with the character., its Unicode code point in decimal or hexadecimal: &#8377; and &#x20B9; are both the rupee sign. A parser replaces every reference with its character, so the program that reads the document never sees the reference itself. Named entities from HTML, such as &nbsp;, are not predefined in XML, and a parser stops on them with undefined entity unless the document declares them.

Attribute values hold one more surprise. A parser normalises them: every line break and tab written in an attribute value is read back as a space. To keep one, write it as the character reference &#10; or &#9;. The program shows both traps, then the two ways to escape correctly, with the helpers of xml.sax.saxutils and by letting ElementTree write the element:

Escaping by hand, with saxutils and with ElementTree Python · escaping/escaping.py
"""Escaping text and attribute values: by hand, with xml.sax.saxutils, and with ElementTree's serializer."""
import xml.etree.ElementTree as ET
from xml.sax.saxutils import escape, quoteattr

name = "Fish & Chips <Kochi>"
note = 'Ask for "extra" lime\nNo onions'

# 1. An f-string with no escaping: not well-formed.
raw = f"<item note=\"{note}\">{name}</item>"
try:
    ET.fromstring(raw)
except ET.ParseError as err:
    print("f-string, no escaping:", err)

# 2. A raw line break inside an attribute value is read back as a space.
print("raw line break read as:", repr(ET.fromstring('<item note="one\ntwo"/>').get("note")))

# 3. escape() for text and quoteattr() for an attribute value (it adds the quotes).
print("escape(name):    ", escape(name))
print("quoteattr(note): ", quoteattr(note))
back = ET.fromstring(f"<item note={quoteattr(note)}>{escape(name)}</item>")
print("read back:       ", repr(back.text), repr(back.get("note")))

# 4. ElementTree escapes for you, and writes the line break as a character reference.
item = ET.Element("item", note=note)
item.text = name
written = ET.tostring(item, encoding="unicode")
print("ElementTree wrote:", written)
again = ET.fromstring(written)
print("same values back:", again.text == name and again.get("note") == note)

# 5. A character reference names any character by its code point.
print(ET.fromstring("<price>&#x20B9;379 or &#8377;379</price>").text)

Output

f-string, no escaping: not well-formed (invalid token): line 1, column 21
raw line break read as: 'one two'
escape(name):     Fish &amp; Chips &lt;Kochi&gt;
quoteattr(note):  'Ask for "extra" lime&#10;No onions'
read back:        'Fish & Chips <Kochi>' 'Ask for "extra" lime\nNo onions'
ElementTree wrote: <item note="Ask for &quot;extra&quot; lime&#10;No onions">Fish &amp; Chips &lt;Kochi&gt;</item>
same values back: True
₹379 or ₹379

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 escaping.py

The f-string without escaping failed at column 21, the e of extra: the quote before it had already ended the attribute value. The raw line break came back as a space. escape() handled the text and quoteattr() the attribute value, choosing single quotes because the note contains double ones and writing the line break as &#10;. ElementTree did the same work on its own and read back exactly the values it was given.

Building XML with string formatting

Wrong: f"<name>{name}</name>", which works until a name contains & or < and then produces a document no parser accepts. Right: build elements with a library such as ElementTree and let it write the text, or pass every text through escape() and every attribute value through quoteattr().

Checkpoint

Check: Escaping text and attribute values

  1. Question 1 of 2 Which characters of the text 5 < 6 & 7 > 3 must be escaped when it is the content of an element? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • &
    • <

    The text becomes 5 &lt; 6 &amp; 7 > 3. Escaping > as well does no harm, and libraries such as escape() do it anyway.

  2. Question 2 of 2 A file holds <item note="one on one line and two"/> on the next. What does a parser return for note?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: one two, with a space where the line break was

    escaping.py read the raw break back as one two. To keep the break, write it as &#10;, which both quoteattr() and ElementTree do.

CDATA is just text; comments and processing instructions are not data

Three kinds of markup look unusual, and only one of them holds data. A CDATA section: A block of text between <![CDATA[ and ]]> in which < and & need no escaping. A parser reports its content as ordinary text, and the section ends at the first ]]>., <![CDATA[ … ]]>, lets you write < and & without escaping them, which is handy for a snippet of code or a rule that people edit by hand. It ends at the first ]]>, so that sequence cannot appear inside it. A comment, <!-- … -->, is a note for people; it may not contain two hyphens in a row. A processing instruction: Markup of the form <?target data?> that passes an instruction to an application, such as xml-stylesheet for a browser. It is not part of the document's data, and many parsers drop it., <?target data?>, passes an instruction to one application, as <?xml-stylesheet … ?> tells a browser which stylesheet to use. The XML declaration looks like one but is not: it may only stand at the start.

This menu uses all three, and the program shows what ElementTree keeps of them:

menu.xml: a comment, a CDATA section and processing instructions XML · markup/menu.xml
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="menu.xsl"?>
<!-- Menu of the Kochi branch -->
<menu>
  <!-- prices include tax -->
  <item price="&#x20B9;250">Fish &amp; Chips</item>
  <rule><![CDATA[show if a < b && open]]></rule>
  <?print-hint bold?>
</menu>
What ElementTree keeps, and what it writes back Python · markup/markup.py
"""What ElementTree keeps of comments, a CDATA section, processing instructions and character references."""
import xml.etree.ElementTree as ET

root = ET.parse("menu.xml").getroot()
for child in root:
    print(f"{child.tag}: {child.text!r} {child.attrib}")
print("written back:")
print(ET.tostring(root, encoding="unicode"))

# A TreeBuilder that keeps comments and processing instructions, but only those inside the root element.
builder = ET.TreeBuilder(insert_comments=True, insert_pis=True)
root = ET.parse("menu.xml", parser=ET.XMLParser(target=builder)).getroot()
print("with insert_comments and insert_pis:")
print(ET.tostring(root, encoding="unicode"))

Output

item: 'Fish & Chips' {'price': '₹250'}
rule: 'show if a < b && open' {}
written back:
<menu>
  
  <item price="₹250">Fish &amp; Chips</item>
  <rule>show if a &lt; b &amp;&amp; open</rule>
  
</menu>
with insert_comments and insert_pis:
<menu>
  <!-- prices include tax -->
  <item price="₹250">Fish &amp; Chips</item>
  <rule>show if a &lt; b &amp;&amp; open</rule>
  <?print-hint bold?>
</menu>

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 markup.py

The CDATA section arrived as ordinary text, and when the tree was written back it came out escaped: show if a &lt; b &amp;&amp; open. To the parser, a CDATA section and escaped text are two spellings of the same characters, so no program should depend on which one a file uses. Comments and processing instructions were dropped, which ElementTree does by default. A TreeBuilder with insert_comments and insert_pis keeps them, but only those inside the root element: the comment and the stylesheet instruction above <menu> were still lost.

Keep data out of comments

A comment that holds data, such as a version or a flag, is invisible to most parsers and gone after a round trip. Put it in an element or an attribute, where every parser reports it.

Checkpoint

Check: CDATA, comments and processing instructions

  1. Question 1 of 2 ElementTree reads <rule><![CDATA[a < b]]></rule> and writes the tree back. What does it write?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: <rule>a &lt; b</rule>

    A CDATA section and escaped text are two spellings of the same characters, as markup.py showed with the rule of the menu.

  2. Question 2 of 2 menu.xml has a comment above <menu> and one inside it. Which ones does a TreeBuilder with insert_comments=True keep?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Only the comment inside the root element

    A tree that starts at the root element has nowhere to hang what stands before it. Data that must survive belongs in an element or an attribute.

Elements or attributes: let the value decide

The same order could be written <item sku="BK-101" qty="2"/> or with <sku> and <qty> elements, and both are well-formed. The choice matters when the data changes, because an attribute can do less than an element. Its name may appear once per tag, its value is a single string, and the specification says the order of attributes is not significant. Mixed content shows the difference most clearly. In directions such as <p>Take <b>exit 4</b> to <i>Station Road</i>, then <b>exit 2</b>.</p>, text and elements alternate, and their order is the meaning:

Mixed content in ElementTree and in a naive JSON conversion Python · mixed/mixed.py
"""Mixed content: text and elements in one order. What ElementTree keeps, and what a naive JSON conversion loses."""
import json
import xml.etree.ElementTree as ET

first = ET.fromstring("<p>Take <b>exit 4</b> to <i>Station Road</i>, then <b>exit 2</b>.</p>")
second = ET.fromstring("<p>Take <i>Station Road</i> to <b>exit 4</b>, then <b>exit 2</b>.</p>")

# ElementTree keeps the order: the text before the first child, and each child's tail after it.
print("text:", repr(first.text))
for child in first:
    print(f"<{child.tag}> text={child.text!r} tail={child.tail!r}")


def naive(element):
    """Elements become keys and the pieces of text are joined: a common shortcut for record-like XML."""
    out = {}
    for child in element:
        out.setdefault(child.tag, []).append(child.text)
    out["#text"] = (element.text or "") + "".join(child.tail or "" for child in element)
    return out


for p in (first, second):
    print("sentence:  ", "".join(p.itertext()))
    print("naive JSON:", json.dumps(naive(p)))
print("same JSON for both sentences:", naive(first) == naive(second))

Output

text: 'Take '
<b> text='exit 4' tail=' to '
<i> text='Station Road' tail=', then '
<b> text='exit 2' tail='.'
sentence:   Take exit 4 to Station Road, then exit 2.
naive JSON: {"b": ["exit 4", "exit 2"], "i": ["Station Road"], "#text": "Take  to , then ."}
sentence:   Take Station Road to exit 4, then exit 2.
naive JSON: {"i": ["Station Road"], "b": ["exit 4", "exit 2"], "#text": "Take  to , then ."}
same JSON for both sentences: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 mixed.py

ElementTree keeps the order: the text before the first child, then each child’s text and its tail, the text that follows it. The naive conversion makes each element name a key and joins the loose text, and two sets of directions that lead to different places, “exit 4 to Station Road” and “Station Road to exit 4”, turned into the same JSON. Books, contracts and documentation are full of mixed content: The content of an element in which text and child elements alternate, such as a paragraph with words in bold. The order of its pieces is part of its meaning., which is where XML does better than JSON; for record-like data, either format works.

For each value of your own document, answer three questions and see which form fits:

Interactive · answer and compare Should this value be an element or an attribute? Answer three questions about one value of your document. The table marks the place that fits and says why.
Can the value appear more than once in the same element?
Might the value need parts of its own later, such as a currency next to an amount?
Is it text for people, with words in bold, links or other markup inside?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

An element and an attribute against five things a value may need
Write it as Appears more than once Gains parts later Text with markup Order matters Short metadata
An element Looks at: <qty>2</qty> inside its parent Good two <phone> elements are fine Good <amount> can gain <currency> and <value> later Good keeps text and elements in order, as <p> did Good elements keep the order they were written in Mixed works, but a whole element for an id is heavy
An attribute Looks at: qty="2" in the start tag Poor a name may appear once per element Poor a value is one string: no structure inside Poor no markup inside a value: < is not allowed Poor the order of attributes is not significant Good ids, codes and units sit next to what they describe
How the answers decide
  • “Can the value appear more than once in the same element?” Yes, there can be several: rules out An attribute. An attribute name may appear only once in a start tag, so a value that repeats needs elements.
  • “Might the value need parts of its own later, such as a currency next to an amount?” Yes, it may grow: rules out An attribute. An attribute value is a single string; a value that may gain parts needs an element to hold them.
  • “Is it text for people, with words in bold, links or other markup inside?” Yes, text with markup: rules out An attribute. Markup cannot sit inside an attribute value, and mixed content needs the order that elements keep.
  • “Can the value appear more than once in the same element?” No, at most one and “Might the value need parts of its own later, such as a currency next to an amount?” No, it stays one string and “Is it text for people, with words in bold, links or other markup inside?” No, plain data: adds a caveat to An element. Both fit. An attribute is the lighter choice for short metadata such as an id, a code or a unit.

In short: elements for values that repeat, may grow parts or carry markup, and attributes for short metadata about an element, such as its id, a code or a unit.

Checkpoint

Check: Elements, attributes and mixed content

  1. One question A customer may have several phone numbers. How do you write them?

    Choose one answer.

    Show the answer: One question

    Answer: One <phone> element for each number

    A value that repeats goes into elements; attributes suit single, short metadata such as an id or a code.

Practise: write one element that parses back exactly

The exercise asks you to write the escaping of this lesson as a function: an element with its attributes and text, which ElementTree must read back exactly, also when the text holds ]]> and an attribute holds a line break:

Exercise · Easy · Python

Write one XML element that parses back exactly

Write element(tag, attrs, text) in element.py. It returns one XML element as a string: the tag, the attributes in the order of the attrs dictionary, and the text. Escape everything, so that a parser reads back exactly the values you were given.

  • In the text, write & as &amp;, < as &lt; and > as &gt; (the last keeps ]]> out of the text).
  • Put every attribute value in double quotes. Escape &, < and ", and write a line break, a carriage return and a tab as the character references &#10;, &#13; and &#9;, because a parser turns a raw one into a space.
  • When text is empty, return an empty-element tag such as <note/>.

The tests parse what your function returns with ElementTree and compare the values with the ones you started from.

Starter code · element.py

def element(tag, attrs, text):
    """One XML element as text, with the attribute values and the text escaped."""
    attributes = "".join(f' {name}="{value}"' for name, value in attrs.items())
    return f"<{tag}{attributes}>{text}</{tag}>"
The sample tests · test_element.py
import xml.etree.ElementTree as ET

from element import element


def test_plain_text():
    """writes a start tag, the text and an end tag"""
    assert element("branch", {}, "Pune") == "<branch>Pune</branch>"


def test_ampersand_and_angle_brackets():
    """escapes & and < in text, so the name parses back as written"""
    written = element("name", {}, "Fish & Chips <Kochi>")
    assert ET.fromstring(written).text == "Fish & Chips <Kochi>"


def test_no_cdata_end_in_text():
    """keeps the sequence ]]> out of the text"""
    written = element("rule", {}, "a]]>b")
    assert "]]>" not in written
    assert ET.fromstring(written).text == "a]]>b"


def test_quotes_in_an_attribute():
    """escapes a double quote inside a double-quoted attribute value"""
    written = element("item", {"note": 'Ask for "extra" lime'}, "Chai")
    assert ET.fromstring(written).get("note") == 'Ask for "extra" lime'


def test_line_break_and_tab_in_an_attribute():
    """writes a line break and a tab as character references, so they survive"""
    written = element("item", {"note": "one\ntwo\tthree"}, "Chai")
    assert ET.fromstring(written).get("note") == "one\ntwo\tthree"


def test_ampersand_and_less_than_in_an_attribute():
    """escapes & and < inside an attribute value, and keeps a carriage return"""
    written = element("rule", {"test": "a < b && open\r"}, "Chai")
    assert ET.fromstring(written).get("test") == "a < b && open\r"


def test_attributes_in_order_and_empty_element():
    """keeps the attributes in order and writes an empty element as a single tag"""
    assert element("item", {"sku": "BK-101", "qty": "2"}, "") == '<item sku="BK-101" qty="2"/>'
A hint

Replace & first. If you turn < into &lt; and only then replace &, the ampersand of &lt; becomes &amp;lt;, and the parser reads back the five characters &lt; instead of <.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

After that, test yourself on the six ideas of the lesson. Each question picks the idea you have shown the least so far, and the reasoning appears as soon as you answer:

Practice that adapts to you

Practise this lesson

17 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 17 Which of these are rules of well-formed XML? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 1 of 17

    Answer:

    • An attribute name appears at most once in a tag
    • Attribute values are in quotes
    • There is exactly one root element

    The six rules are about structure and spelling: one root, matching and nested tags, quoted unique attributes, escaped < and &, and the declaration first.

  2. Question 2 of 17 This file starts with an empty line, then the XML declaration. What does ElementTree do with it?

    Read the code, then choose one answer.

    
    <?xml version="1.0"?>
    <order/>
    Show the answer: Question 2 of 17

    Answer: It stops with XML or text declaration not at start of entity, at line 2

    Remove the blank line, or remove the declaration: it is optional, and UTF-8 is assumed without it.

  3. Question 3 of 17 Which of these is a well-formed element name?

    Choose one answer.

    Show the answer: Question 3 of 17

    Answer: floor-1

    A name starts with a letter or an underscore. Names that start with xml are reserved, and the colon is kept for namespaces.

  4. Question 4 of 17 What must an XML processor do after it finds a fatal error?

    Choose one answer.

    Show the answer: Question 4 of 17

    Answer: Stop normal processing: no more data is passed on as usual

    This is why one bare ampersand loses a whole feed: the parser does not hand over the items it read before the error as if nothing had happened.

  5. Question 5 of 17 For <name>Fish & Chips</name>, ElementTree reported column 14. Counting from 0, which character is that?

    Choose one answer.

    Show the answer: Question 5 of 17

    Answer: The space after the ampersand

    A position marks where the parser noticed a problem. For a bare ampersand that is one character after it; for a missing end tag it can be many lines later.

  6. Question 6 of 17 After the mismatched tag error at </items>, a developer deleted line 4. Now ElementTree says no element found. Why?

    Choose one answer.

    Show the answer: Question 6 of 17

    Answer: The document now ends while items and an item are still open

    The fix is above the reported line: close the item after Pens. Editing the named line moves the error instead of removing it.

  7. Question 7 of 17 What does it mean for an XML document to be valid?

    Choose one answer.

    Show the answer: Question 7 of 17

    Answer: It is well-formed and also follows a schema for its kind of document

    Every valid document is well-formed, and many well-formed documents are not valid for the program that reads them.

  8. Question 8 of 17 An order must have an amount equal to the sum of its items. The shop's schema lists the elements of an order and their types, nothing more. Which check enforces the sum?

    Choose one answer.

    Show the answer: Question 8 of 17

    Answer: The program's own checks, after parsing and validation

    Each check catches what the earlier ones cannot see: the parser the syntax, the schema the shape and types, your code the rules the schema does not state. An XSD 1.1 assertion could state this one in the schema instead.

  9. Question 9 of 17 When must > be escaped in the text of an element?

    Choose one answer.

    Show the answer: Question 9 of 17

    Answer: When it is part of the sequence ]]>

    Most libraries escape every > anyway, which is always allowed and never wrong.

  10. Question 10 of 17 Text copied from a web page contains &nbsp;, and the parser stops with undefined entity. What is the simplest fix?

    Choose one answer.

    Show the answer: Question 10 of 17

    Answer: Write the character reference &#160; instead

    HTML's named entities are not part of XML. Character references work for every character without a DTD.

  11. Question 11 of 17 This function escapes text. What does it return for a < b, and why?

    Read the code, then choose one answer.

    def escape_text(s):
        return s.replace("<", "&lt;").replace("&", "&amp;")
    Show the answer: Question 11 of 17

    Answer: a &amp;lt; b: the second replace escapes the & that the first one wrote

    A parser would read back the text a &lt; b, five characters where one was meant. escape() and ElementTree get the order right for you.

  12. Question 12 of 17 Which text cannot appear inside a CDATA section?

    Choose one answer.

    Show the answer: Question 12 of 17

    Answer: ]]>

    To put ]]> in text, end the section between ]] and >, or escape the text instead of using CDATA.

  13. Question 13 of 17 Which comment is well-formed?

    Choose one answer.

    Show the answer: Question 13 of 17

    Answer: <!-- prices include tax -->

    The specification forbids -- inside comments. Keep comments short notes for people, and keep data out of them.

  14. Question 14 of 17 A program needs the stylesheet named by <?xml-stylesheet href="menu.xsl"?> above the root. What happens with ElementTree's default parser?

    Choose one answer.

    Show the answer: Question 14 of 17

    Answer: It is dropped: the default parser skips processing instructions

    A program that needs instructions outside the root uses an event or DOM parser that reports them, or reads them before parsing.

  15. Question 15 of 17 Which of these are true of attributes? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 15 of 17

    Answer:

    • Their order is not significant
    • A name appears at most once in a tag

    These limits are why values that repeat, grow or carry markup belong in elements.

  16. Question 16 of 17 mixed.py converted two sets of directions, "exit 4 to Station Road" and "Station Road to exit 4", with a naive converter. What did it find?

    Choose one answer.

    Show the answer: Question 16 of 17

    Answer: Both gave the same JSON, so the order of words and elements was lost

    Mixed content needs a model that keeps order, such as ElementTree's text and tail, or a JSON shape built for it, such as a list of text and element nodes.

  17. Question 17 of 17 An order's id is a short code that never repeats and never gains parts. A colleague wants <id> elements for consistency. What do you say?

    Choose one answer.

    Show the answer: Question 17 of 17

    Answer: Both are well-formed; an attribute is the lighter choice for short metadata like an id

    Choose by the value: repeats, parts or markup call for elements; a single short code fits an attribute. Then keep the choice the same across the format.

Interview questions

Warm-up, for freshers and up to 5 years: how does a well-formed XML document differ from a valid one?

A well-formed document follows the syntax of XML itself: one root element, every start tag matched by an end tag in the right order, attribute values in quotes with no name repeated, and < and & escaped in text. Every XML parser checks this, and a document that is not well-formed is not XML at all, so the parser stops with a fatal error. A valid document is well-formed and also follows a schema for its kind of document, a DTD, an XSD or a RELAX NG grammar, which says which elements and attributes may appear, in which order and with which types. Validity is checked only when a program asks a validating parser to do it. So <qty>two</qty> is well-formed, and it is invalid only against a schema that says a quantity is a whole number.

Two harder questions follow: a feed that one ampersand broke, and the design of an export that people and programs both read. Settle on what you would do first in each case, then follow the question’s link and compare it with the first step given there:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A partner's nightly product feed of 40,000 items fails to import: the parser reports not well-formed (invalid token) at line 18,233, where an item is named Fish & Chips. The partner asks you to make the import skip bad items. What do you do tonight, and what do you change so that it does not happen again?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    You are designing an XML export of orders that a partner's program reads and support staff open in a browser. Each order has an id, a currency, several items, a delivery note that may contain bold text and links, and may later gain a discount breakdown. Which values become attributes and which elements, and why?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What makes an XML document well-formed?

    One root element; end tags that close the element opened last; attribute values in quotes, with no name repeated in a tag; < and & escaped; and the XML declaration first or not at all.

  2. Why can an XML parser report a missing end tag lines after the mistake?

    It stops where it notices the problem, which can be later than where it was made. ElementTree counts lines from 1 and columns from 0.

  3. How does a valid XML document differ from a well-formed one?

    Every parser checks well-formedness, a property of the text. Validity also needs a schema, a DTD, an XSD or a RELAX NG grammar, and is checked only when a program asks.

  4. Where must <, &, >, " and ' be escaped in XML?

    < and & everywhere; > only in the sequence ]]>; " and ' only inside an attribute value delimited by that quote.

  5. How do you keep a line break inside an XML attribute value?

    Write it as the character reference &#10;. A raw line break in an attribute value is read back as a space.

  6. What is a CDATA section in the data a parser returns?

    Just text, written without escapes. Comments and processing instructions are not data at all, and ElementTree drops them by default.

  7. When should a value be an element rather than an attribute?

    When it repeats, grows or carries markup. Attributes suit short metadata, and mixed content needs elements because their order carries the meaning.

Key takeaways

  • A well-formed document has one root, end tags that match the element opened last, quoted attribute values with no name repeated, escaped < and &, and the XML declaration first or not at all.
  • A parser stops at its first fatal error and reports where it noticed it, which for a missing end tag can be lines after the mistake; ElementTree counts lines from 1 and columns from 0.
  • Well-formed is checked by every parser; valid means the document also follows a schema, and your own code checks the rules your schema does not state.
  • Escape < and & everywhere, > in ]]>, and the quote that delimits an attribute; write line breaks in attribute values as &#10;, or let a library write the XML.
  • CDATA sections are another spelling of text, comments and processing instructions are not data, and ElementTree drops the last two by default.
  • Use elements for values that repeat, grow or carry markup, and attributes for short metadata; mixed content needs elements because their order is the meaning.

Terms in this lesson

CDATA section
A block of text between <![CDATA[ and ]]> in which < and & need no escaping. A parser reports its content as ordinary text, and the section ends at the first ]]>.
character reference
A character written by its Unicode code point between &# and ;, in decimal or in hexadecimal after &#x, so &#8377; and &#x20B9; are both the rupee sign. A parser replaces it with the character.
entity reference
A name between & and ; that stands for text, such as &lt; for < and &amp; for &. XML predefines five of them; any other must be declared in the document's DTD.
mixed content
The content of an element in which text and child elements alternate, such as a paragraph with words in bold. The order of its pieces is part of its meaning.
processing instruction
Markup of the form <?target data?> that passes an instruction to an application, such as xml-stylesheet for a browser. It is not part of the document's data, and many parsers drop it.
root element
The one element of an XML document that contains all the others, also called the document element. Comments and processing instructions may stand outside it, but no other element may.
valid
Of an XML document: well-formed and also following a schema, such as a DTD or an XSD, that says which elements and attributes the document has, in which order and of which types.
well-formed
Following the syntax rules of XML 1.0: one root element, tags that match and nest, quoted attribute values with no name repeated, and < and & escaped. A parser refuses any document that is not well-formed.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.