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 &nbsp;":     "<a>1&nbsp;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 &nbsp;       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:

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 &nbsp; &eacute;
parser = ET.XMLParser()
parser.entity.update({"nbsp": "\u00a0", "eacute": "\u00e9"})
try:
    ET.fromstring("<a>caf&eacute;&nbsp;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&eacute;&nbsp;1 &amp; 2</a>")
print("numeric refs:", fixed, "->", repr(ET.fromstring(fixed).text))
print("html.unescape breaks &amp;:", html.unescape("<a>A &amp; 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 &amp; Chips &lt;large&gt;</item>
ET.tostring(): <item>Fish &amp; Chips &lt;large&gt;</item>
parser.entity alone: undefined entity: line 1, column 6
numeric refs: <a>caf&#233;&#160;1 &amp; 2</a> -> 'café\xa01 & 2'
html.unescape breaks &amp;: <a>A & B</a>
wrapped: ['1', '2']

Measured:

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 &nbsp; that XML doesn't define. We measured that converting named entities to numeric references (&#160;) 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.