diff --git a/Lib/plistlib.py b/Lib/plistlib.py
index 93f3ef5e38af84..9c0ca6dc5f151b 100644
--- a/Lib/plistlib.py
+++ b/Lib/plistlib.py
@@ -67,7 +67,7 @@
import os
import re
import struct
-from xml.parsers.expat import ParserCreate
+from xml.parsers.expat import ExpatError, ParserCreate
PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__)
@@ -185,7 +185,20 @@ def parse(self, fileobj):
self.parser.EndElementHandler = self.handle_end_element
self.parser.CharacterDataHandler = self.handle_data
self.parser.EntityDeclHandler = self.handle_entity_decl
- self.parser.ParseFile(fileobj)
+ try:
+ self.parser.ParseFile(fileobj)
+ except ExpatError as e:
+ raise InvalidFileException(str(e)) from e
+ except LookupError as e:
+ # An declaration naming an encoding that Python's
+ # codec registry does not know raises a plain LookupError. A
+ # KeyError or IndexError from caller-provided code (e.g. a custom
+ # dict_type or file object) is a LookupError subclass and must not
+ # be masked as a malformed-file error, so only translate the exact
+ # LookupError type and let subclasses propagate unchanged.
+ if type(e) is not LookupError:
+ raise
+ raise InvalidFileException(str(e)) from e
return self.root
def handle_entity_decl(self, entity_name, is_parameter_entity, value, base, system_id, public_id, notation_name):
diff --git a/Lib/test/test_plistlib.py b/Lib/test/test_plistlib.py
index b9c261310bb567..8c61f68eb0521a 100644
--- a/Lib/test/test_plistlib.py
+++ b/Lib/test/test_plistlib.py
@@ -916,6 +916,36 @@ def test_load_invalid_file(self):
with self.assertRaises(plistlib.InvalidFileException):
plistlib.loads(b"these are not plist file contents")
+ def test_xml_plist_not_well_formed(self):
+ # gh-155397: a not-well-formed XML plist must raise the documented
+ # InvalidFileException, not a raw xml.parsers.expat.ExpatError.
+ with self.assertRaises(plistlib.InvalidFileException):
+ plistlib.loads(b"", fmt=plistlib.FMT_XML)
+
+ def test_xml_plist_unknown_encoding(self):
+ # gh-155397: an declaration naming an encoding that
+ # Python does not know must raise the documented InvalidFileException,
+ # not a raw LookupError.
+ data = b''
+ with self.assertRaises(plistlib.InvalidFileException):
+ plistlib.loads(data, fmt=plistlib.FMT_XML)
+
+ def test_xml_plist_dict_type_lookup_error_propagates(self):
+ # gh-155397: a KeyError/IndexError raised by caller-supplied code (here
+ # a custom dict_type) is a LookupError subclass but signals a bug in the
+ # caller, not a malformed file. It must propagate unchanged rather than
+ # be masked as InvalidFileException, matching the pre-fix behaviour and
+ # the binary parser. Only a plain codec-registry LookupError (unknown
+ # declared encoding) is translated.
+ class RaisingDict(dict):
+ def __setitem__(self, key, value):
+ raise KeyError("boom from caller code")
+
+ data = (b''
+ b'ab')
+ with self.assertRaises(KeyError):
+ plistlib.loads(data, fmt=plistlib.FMT_XML, dict_type=RaisingDict)
+
def test_modified_uid_negative(self):
neg_uid = UID(1)
neg_uid.data = -1 # dodge the negative check in the constructor
diff --git a/Misc/NEWS.d/next/Library/2026-08-21-17-45-42.gh-issue-155397.wet1xZ.rst b/Misc/NEWS.d/next/Library/2026-08-21-17-45-42.gh-issue-155397.wet1xZ.rst
new file mode 100644
index 00000000000000..03ec877a0773ac
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-21-17-45-42.gh-issue-155397.wet1xZ.rst
@@ -0,0 +1 @@
+:func:`plistlib.load` and :func:`plistlib.loads` now raise :exc:`plistlib.InvalidFileException` for XML plists that are not well-formed or that declare an unknown encoding, instead of leaking the underlying :exc:`~xml.parsers.expat.ExpatError` or :exc:`LookupError`.