xml.etree ParseError in Python: every message explained (tested)
xml.etree.ElementTree.ParseError tells you the line and column, but not what to do. We ran ten broken inputs and seven encoding cases through ElementTree to map each message to its cause, then tested fixes, including one common fix that doesn't work.
1. What each ParseError message means
import xml.etree.ElementTree as ET
cases = {
"empty string": "",
"unclosed tag": "<a><b>text</a>",
"bare ampersand": "<a>Tom & Jerry</a>",
"HTML entity ": "<a>1 000</a>",
"two root elements": "<a>1</a><a>2</a>",
"text before the root": "Error 502<a/>",
"whitespace before <?xml": "\n<?xml version='1.0'?><a/>",
"control character": "<a>bell\x07</a>",
"unquoted attribute": "<a id=1/>",
"HTML page": "<!DOCTYPE html><html><body><br></body></html>",
}
for label, text in cases.items():
try:
ET.fromstring(text)
print(f"{label:24} OK")
except ET.ParseError as e:
print(f"{label:24} ParseError: {e} | code={e.code} position={e.position}")
Output:
empty string ParseError: no element found: line 1, column 0 | code=3 position=(1, 0)
unclosed tag ParseError: mismatched tag: line 1, column 12 | code=7 position=(1, 12)
bare ampersand ParseError: not well-formed (invalid token): line 1, column 8 | code=4 position=(1, 8)
HTML entity ParseError: undefined entity: line 1, column 4 | code=11 position=(1, 4)
two root elements ParseError: junk after document element: line 1, column 8 | code=9 position=(1, 8)
text before the root ParseError: syntax error: line 1, column 0 | code=2 position=(1, 0)
whitespace before <?xml ParseError: XML or text declaration not at start of entity: line 2, column 0 | code=17 position=(2, 0)
control character ParseError: not well-formed (invalid token): line 1, column 7 | code=4 position=(1, 7)
unquoted attribute ParseError: not well-formed (invalid token): line 1, column 6 | code=4 position=(1, 6)
HTML page ParseError: mismatched tag: line 1, column 33 | code=7 position=(1, 33)
Measured on Python 3.13.5: the message names the problem and e.position gives (line, column), with the column counted from 0. Quick lookup:
no element found: empty input (a failed download, or a file read twice). Also a document cut off before its end.not well-formed (invalid token): a bare&, a control character, an unquoted attribute, or bytes in the wrong encoding (section 2). For the bare&the column pointed just past it.undefined entity: an HTML entity such as . XML only knows& < > " '.mismatched tag: an unclosed element, or HTML (whose<br>has no closing tag). ElementTree is not an HTML parser.junk after document element: more than one root element.syntax errorat column 0: the input doesn't start with XML at all, e.g. a plain-text error from a server.XML or text declaration not at start of entity: whitespace or a blank line before<?xml.
2. Encoding: pass bytes, not a mis-decoded string
import codecs, xml.etree.ElementTree as ET
cases = {
"str with encoding decl": "<?xml version='1.0' encoding='utf-8'?><a>é</a>",
"utf-8 bytes, no decl": "<a>é</a>".encode("utf-8"),
"latin-1 bytes, no decl": "<a>é</a>".encode("latin-1"),
"latin-1 bytes, correct decl": "<?xml version='1.0' encoding='iso-8859-1'?><a>é</a>".encode("latin-1"),
"utf-8 bytes with BOM": codecs.BOM_UTF8 + "<a>é</a>".encode("utf-8"),
"BOM decoded into a str": (codecs.BOM_UTF8 + b"<a/>").decode("latin-1"),
"utf-16 bytes": "<a>é</a>".encode("utf-16"),
}
for label, data in cases.items():
try:
print(f"{label:28} OK text={ET.fromstring(data).text!r}")
except ET.ParseError as e:
print(f"{label:28} ParseError: {e}")
Output:
str with encoding decl OK text='é'
utf-8 bytes, no decl OK text='é'
latin-1 bytes, no decl ParseError: not well-formed (invalid token): line 1, column 3
latin-1 bytes, correct decl OK text='é'
utf-8 bytes with BOM OK text='é'
BOM decoded into a str ParseError: not well-formed (invalid token): line 1, column 1
utf-16 bytes OK text='é'
Measured: the parser decodes bytes itself using the BOM or the encoding= declaration, so UTF-8, UTF-16 and correctly declared Latin-1 all worked. Latin-1 bytes without a declaration fail with not well-formed (invalid token), and so does a BOM that was decoded into the string as text. A str with an encoding= declaration is accepted by ElementTree. With requests, pass r.content (bytes) rather than r.text, whose encoding requests guesses from the headers.
3. Fixes: find the spot, escape, entities, fragments
import html, re, xml.etree.ElementTree as ET
from xml.sax.saxutils import escape
def parse_or_explain(text):
try:
return ET.fromstring(text)
except ET.ParseError as e:
line, col = e.position
bad = text.splitlines()[line - 1] if text else ""
print(f"ParseError: {e}")
print(" " + bad[max(0, col - 20):col + 20])
print(" " + " " * min(col, 20) + "^")
# 1) show where it broke
parse_or_explain("<order><item>Fish & Chips</item></order>")
# 2) escape text when you BUILD the XML (ElementTree does it for you)
name = "Fish & Chips <large>"
print("escape():", f"<item>{escape(name)}</item>")
el = ET.Element("item"); el.text = name
print("ET.tostring():", ET.tostring(el, encoding="unicode"))
# 3) HTML entities like é
parser = ET.XMLParser()
parser.entity.update({"nbsp": "\u00a0", "eacute": "\u00e9"})
try:
ET.fromstring("<a>café 1</a>", parser=parser)
except ET.ParseError as e:
print("parser.entity alone:", e)
from html.entities import name2codepoint
XML_OWN = {"amp", "lt", "gt", "quot", "apos"}
def html_entities_to_numeric(text):
return re.sub(r"&([A-Za-z][A-Za-z0-9]*);",
lambda m: f"&#{name2codepoint[m[1]]};"
if m[1] in name2codepoint and m[1] not in XML_OWN else m[0],
text)
fixed = html_entities_to_numeric("<a>café 1 & 2</a>")
print("numeric refs:", fixed, "->", repr(ET.fromstring(fixed).text))
print("html.unescape breaks &:", html.unescape("<a>A & B</a>"))
# 4) several root elements (a fragment / XML stream): wrap them
frag = "<a>1</a><a>2</a>"
print("wrapped:", [x.text for x in ET.fromstring(f"<root>{frag}</root>")])
Output:
ParseError: not well-formed (invalid token): line 1, column 19
<order><item>Fish & Chips</item></order
^
escape(): <item>Fish & Chips <large></item>
ET.tostring(): <item>Fish & Chips <large></item>
parser.entity alone: undefined entity: line 1, column 6
numeric refs: <a>café 1 & 2</a> -> 'café\xa01 & 2'
html.unescape breaks &: <a>A & B</a>
wrapped: ['1', '2']
Measured:
e.positionlets you print the surrounding text, which is faster than counting columns by hand.- If you generate the XML, never paste text into tags with f-strings.
xml.sax.saxutils.escape()or settingel.textand callingET.tostring()both producedFish & Chips <large>. - Filling
XMLParser().entitydid not fixundefined entityfor a document without a DOCTYPE; we still got the error. (In a separate one-liner it worked only once we added an external<!DOCTYPE a PUBLIC "x" "y">.) Converting named HTML entities to numeric references before parsing worked and kept&intact.html.unescape()on the whole document is wrong: it turns&into a bare&, which is the next ParseError. - For several top-level elements (a log of XML records), wrap them in one root before parsing.
ET.ParseError subclasses SyntaxError, not ValueError, so except ValueError doesn't catch it. For real-world HTML use an HTML parser (html.parser or BeautifulSoup) instead of ElementTree. For files that are empty because a call returned None, see our no element found guide.
Frequently asked questions
What does xml.etree.ElementTree.ParseError: not well-formed (invalid token) mean?
A character the XML parser can't accept at that position. We measured it for a bare &, a control character, an unquoted attribute, and Latin-1 bytes without an encoding declaration.
How do I fix ParseError: undefined entity?
The document uses an HTML entity like that XML doesn't define. We measured that converting named entities to numeric references ( ) before parsing works; XMLParser().entity alone did not without an external DOCTYPE.
What does ParseError: junk after document element mean?
There is more than one root element. We measured it for <a>1</a><a>2</a>; wrapping the fragment in a single root element fixes it.
What does ParseError: no element found mean?
The input is empty or ends before the root element closes. We measured an empty string giving 'no element found: line 1, column 0'.
How do I get the line and column of an XML parse error?
Use e.position, a (line, column) tuple with a 0-based column, and e.code for the expat error number. We used it to print the text around the error.
Why does except ValueError not catch ET.ParseError?
ParseError subclasses SyntaxError. Catch ET.ParseError directly.
Written with AI assistance. Measured on 2026-10-09 on Debian with Python 3.13.5's xml.etree.ElementTree: ten malformed inputs with their message, code and position, seven encoding variants, escaping, entity handling and fragment wrapping. Every block was run verbatim and its output pasted from that run. PlainHeaders is our extension; Python's ElementTree is built in.