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.
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:
<?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 & 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:
- 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. - 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>. - Elements nest: the element opened last is closed first, so
<b><i>sale</b></i>is not allowed. - Every attribute value is in quotes, single or double, and an attribute name appears at most once in a tag.
- The characters
<and&never appear on their own in text or in attribute values; the next sections show how to write them. - 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 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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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
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:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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.
- 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:
"""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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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 &. 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.
Checkpoint
Check: Fatal errors and their positions
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 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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
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 < for < and & 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 |
|---|---|---|
< |
< |
always, in text and in attribute values |
& |
& |
always, in text and in attribute values |
> |
> |
only in the sequence ]]> in text; elsewhere it may stay |
" |
" |
inside an attribute value in double quotes |
' |
' |
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 ₹ and ₹ are both the rupee sign. A parser replaces it with the character., its Unicode code
point in decimal or hexadecimal: ₹ and ₹ 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 , 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 or 	. 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 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>₹379 or ₹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 & Chips <Kochi> quoteattr(note): 'Ask for "extra" lime No onions' read back: 'Fish & Chips <Kochi>' 'Ask for "extra" lime\nNo onions' ElementTree wrote: <item note="Ask for "extra" lime No onions">Fish & Chips <Kochi></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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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 . 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
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:
<?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="₹250">Fish & Chips</item>
<rule><![CDATA[show if a < b && open]]></rule>
<?print-hint bold?>
</menu> """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 & Chips</item>
<rule>show if a < b && open</rule>
</menu>
with insert_comments and insert_pis:
<menu>
<!-- prices include tax -->
<item price="₹250">Fish & Chips</item>
<rule>show if a < b && 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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The CDATA section arrived as ordinary text, and when the tree was written back it came out escaped:
show if a < b && 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
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: 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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| 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
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&,<as<and>as>(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 , and	, because a parser turns a raw one into a space. - When
textis 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 < and only then replace &, the ampersand of < becomes &lt;, and the parser reads back the five characters < instead of <.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
-
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?
-
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?
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
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.
-
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.
-
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.
-
Where must <, &, >, " and ' be escaped in XML?
< and & everywhere; > only in the sequence ]]>; " and ' only inside an attribute value delimited by that quote.
-
How do you keep a line break inside an XML attribute value?
Write it as the character reference . A raw line break in an attribute value is read back as a space.
-
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.
-
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 , 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 ₹ and ₹ are both the rupee sign. A parser replaces it with the character.
- entity reference
- A name between & and ; that stands for text, such as < for < and & 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
- Extensible Markup Language (XML) 1.0 (Fifth Edition) (W3C)
- XML 1.0: Terminology (fatal errors) (W3C)
- XML 1.0: Well-formed XML documents (W3C)
- XML 1.0: Common syntactic constructs (names) (W3C)
- XML 1.0: Character data and markup (W3C)
- XML 1.0: Comments (W3C)
- XML 1.0: Processing instructions (W3C)
- XML 1.0: CDATA sections (W3C)
- XML 1.0: Prolog and document type declaration (W3C)
- XML 1.0: Start-tags, end-tags and empty-element tags (W3C)
- XML 1.0: Attribute-value normalization (W3C)
- XML 1.0: Predefined entities (W3C)
- xml.etree.ElementTree: the ElementTree XML API (Python Software Foundation)
- xml.sax.saxutils: SAX utilities (escape and quoteattr) (Python Software Foundation)
- Associating Style Sheets with XML documents 1.0 (Second Edition) (W3C)
- XSD 1.1 Part 1: Structures, assertions (W3C)
- RELAX NG Specification (OASIS)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress