尚未翻译
本页面尚未翻译成中文,因此显示英文原文。 帮助翻译
plistlib Module Complexity¶
The plistlib module reads and writes Apple property lists in two formats: XML, and a compact
binary format. Reading always builds the whole value; XML reaches
the expat parser from the file in chunks, and binary reading seeks from object to object. Writing
XML emits each element to the file as it reaches it, while writing binary first tables every
object in the value so that equal scalars are stored once.
n is the bytes of the serialized plist, read or written. v is the size of the value being
written: its objects plus the characters and bytes of its strings and data. m is the objects
alone, counting every container, key and scalar each time it occurs. d is the keys in one
dictionary, and s is the bytes of the largest single string or data value. Binary output can be
far smaller than v, because equal strings, bytes, numbers and dates of the same type are
written once. Key comparison under sort_keys is
treated as O(1).
Complexity Reference¶
Reading¶
| Operation | Time | Space | Notes |
|---|---|---|---|
plistlib.load(fp, *, fmt=None, dict_type=dict, aware_datetime=False) |
O(n) | O(n) | Space is the returned value, not the file; fmt=None reads 32 bytes to detect the format and seeks back, so fp must be seekable unless fmt=FMT_XML is given. dict_type is called once per dictionary |
plistlib.loads(data, *, fmt=None, dict_type=dict, aware_datetime=False) |
O(n) | O(n) | load() over a BytesIO; from 3.13 data may also be a str of XML |
Writing¶
| Operation | Time | Space | Notes |
|---|---|---|---|
plistlib.dump(value, fp, *, fmt=FMT_XML, sort_keys=True, skipkeys=False, aware_datetime=False) |
O(n) | O(s) | Writes each element to fp as it reaches it, so the document is never held; plus the key sort below |
plistlib.dump(value, fp, fmt=FMT_BINARY) |
O(v) | O(m + s) | Tables every object before writing any, and hashes every scalar to store equal ones once; a bytearray is tabled by identity |
plistlib.dumps(value, *, fmt=FMT_XML, skipkeys=False, sort_keys=True, aware_datetime=False) |
O(n) XML, O(v) binary | O(n) | dump() into a BytesIO; the whole plist is returned as one bytes |
sort_keys=True |
O(d log d) per dictionary | O(d) per open dictionary | The default: each dictionary's items are sorted before it is written. sort_keys=False writes insertion order and skips the sort |
skipkeys=True |
O(1) per skipped key | Unchanged | A key that is not a str is skipped instead of raising TypeError; some versions, 3.15.0 among them, require sort_keys=False for mixed key types (see Version Notes) |
aware_datetime=True |
O(1) per date | O(1) | Python 3.13+: dates are read as UTC-aware datetimes, and aware ones are converted to UTC when written |
UID¶
| Operation | Time | Space | Notes |
|---|---|---|---|
plistlib.UID(data) |
O(1) | O(1) | Wraps an int with 0 <= data < 2**64: another int raises ValueError, a non-int TypeError; binary format only, the XML writer raises TypeError |
UID.data |
O(1) | O(1) | The wrapped int |
Formats and exceptions¶
| Operation | Time | Space | Notes |
|---|---|---|---|
plistlib.FMT_XML, plistlib.FMT_BINARY |
O(1) | O(1) | Members of the plistlib.PlistFormat enum; FMT_XML is the writers' default |
plistlib.InvalidFileException |
O(1) | O(1) | A ValueError subclass, raised for an unrecognised header, a malformed binary plist, or an XML entity declaration. Malformed XML raises xml.parsers.expat.ExpatError instead |
Reading Plists¶
Loading from Bytes¶
loads() detects the format from the first bytes, so the same call reads XML and binary.
import plistlib
plist_bytes = b"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Name</key>
<string>John Doe</string>
<key>Age</key>
<integer>30</integer>
</dict>
</plist>"""
data = plistlib.loads(plist_bytes) # O(n)
assert data == {'Name': 'John Doe', 'Age': 30}
binary = plistlib.dumps(data, fmt=plistlib.FMT_BINARY)
assert plistlib.loads(binary) == data # O(n) - detected as binary
Loading from a File¶
load() needs a binary file object. Without fmt, it reads the first 32 bytes to detect the
format and then seeks back, so a stream that cannot seek must pass fmt=FMT_XML. The binary
format needs a seekable stream either way.
import io
import plistlib
xml_bytes = plistlib.dumps({'version': 2})
with io.BytesIO(xml_bytes) as f:
assert plistlib.load(f) == {'version': 2} # O(n)
class NoSeek(io.RawIOBase):
"""A pipe-like stream: readable, not seekable."""
def __init__(self, data):
self._data = io.BytesIO(data)
def readable(self):
return True
def readinto(self, buffer):
chunk = self._data.read(len(buffer))
buffer[:len(chunk)] = chunk
return len(chunk)
try:
plistlib.load(NoSeek(xml_bytes))
except io.UnsupportedOperation:
pass
else:
raise AssertionError('format detection did not seek')
assert plistlib.load(NoSeek(xml_bytes), fmt=plistlib.FMT_XML) == {'version': 2}
Data Types¶
What Round-trips¶
The documented types are str, int, float, bool, bytes, datetime.datetime, dict
and list, plus UID in the binary format.
Writing also accepts tuple and bytearray, which read back as list and bytes. Dictionary
keys must be strings. Integers must fit in 64 bits, and XML dates keep whole seconds only.
import datetime
import plistlib
when = datetime.datetime(2024, 1, 2, 3, 4, 5, 678901)
value = {
'name': 'MyApp',
'count': 42,
'ratio': 0.5,
'enabled': True,
'items': ('a', 'b'),
'blob': bytearray(b'\x00\x01'),
'when': when,
}
restored = plistlib.loads(plistlib.dumps(value)) # O(n) each way
assert restored['items'] == ['a', 'b'] # tuple -> list
assert restored['blob'] == b'\x00\x01' # bytearray -> bytes
assert restored['when'] == when.replace(microsecond=0) # XML keeps whole seconds
binary = plistlib.loads(plistlib.dumps(value, fmt=plistlib.FMT_BINARY))
assert binary['when'] == when # binary stores float seconds: microseconds kept here
for bad, error in (({1: 'a'}, TypeError), ({'a': {1, 2}}, TypeError), (2**64, OverflowError)):
try:
plistlib.dumps(bad)
except error:
pass
else:
raise AssertionError(f'{bad!r} was written')
Writing Plists¶
Sorted Keys¶
sort_keys=True is the default, so every dictionary is sorted before it is written: O(d log d)
per dictionary. Pass sort_keys=False to keep insertion order and skip the sort.
import plistlib
settings = {'theme': 'dark', 'autosave': True, 'window': [1024, 768]}
sorted_xml = plistlib.dumps(settings) # O(n) plus O(d log d)
as_given = plistlib.dumps(settings, sort_keys=False) # O(n)
assert sorted_xml.index(b'autosave') < sorted_xml.index(b'theme')
assert as_given.index(b'theme') < as_given.index(b'autosave')
assert plistlib.loads(sorted_xml) == plistlib.loads(as_given)
# Disable sorting to skip non-string keys on every supported version
mixed = {'a': 1, 2: 'b'}
assert plistlib.loads(plistlib.dumps(mixed, skipkeys=True, sort_keys=False)) == {'a': 1}
XML vs Binary¶
The two writers differ in what they hold. XML dump() writes each element as it reaches it, so
writing to a file holds one element and the keys being sorted, never the document. The binary
writer tables every object first, which is O(m) memory even when writing to a file, and in return
stores equal scalars once: repeated strings and bytes cost one copy in the output, and read back
as one shared object.
import plistlib
# 100 equal payloads, each a separate object
records = [{'status': 'active', 'payload': bytes(1_000)} for _ in range(100)]
assert records[0]['payload'] is not records[99]['payload']
xml_bytes = plistlib.dumps(records, fmt=plistlib.FMT_XML) # O(n)
binary_bytes = plistlib.dumps(records, fmt=plistlib.FMT_BINARY) # O(v)
assert len(binary_bytes) < len(xml_bytes) // 50 # one payload instead of 100
loaded = plistlib.loads(binary_bytes) # O(n)
assert loaded == records
assert loaded[0]['payload'] is loaded[99]['payload'] # stored once, read back shared
UID and Keyed Archives¶
UID is the reference type of NSKeyedArchiver archives. It exists only in the binary format.
import plistlib
archive = {'$top': {'root': plistlib.UID(1)}, '$objects': ['$null', 'hello']}
data = plistlib.dumps(archive, fmt=plistlib.FMT_BINARY) # O(v)
uid = plistlib.loads(data)['$top']['root']
assert uid == plistlib.UID(1) and uid.data == 1 # O(1)
try:
plistlib.dumps(archive, fmt=plistlib.FMT_XML)
except TypeError as error:
assert 'UID' in str(error)
else:
raise AssertionError('the XML writer accepted a UID')
try:
plistlib.UID(-1)
except ValueError:
pass
else:
raise AssertionError('a negative UID was accepted')
Invalid Input¶
InvalidFileException covers input that is not a plist at all and binary plists that do not
parse. Malformed XML is reported by expat.
import plistlib
from xml.parsers.expat import ExpatError
for bad in (b'not a plist', b'bplist00' + bytes(40)):
try:
plistlib.loads(bad)
except plistlib.InvalidFileException:
pass
else:
raise AssertionError(f'{bad!r} was parsed')
assert issubclass(plistlib.InvalidFileException, ValueError)
try:
plistlib.loads(b'<plist><array></plist>')
except ExpatError as error:
assert 'mismatched tag' in str(error)
else:
raise AssertionError('mismatched tags were parsed')
Common Patterns¶
Read, Modify, Write¶
import pathlib
import plistlib
import tempfile
with tempfile.TemporaryDirectory() as directory:
path = pathlib.Path(directory) / 'settings.plist'
with path.open('wb') as f:
plistlib.dump({'version': 1, 'recent': []}, f) # O(n)
with path.open('rb') as f:
settings = plistlib.load(f) # O(n)
settings['version'] += 1
settings['recent'].append('/tmp/report.txt')
with path.open('wb') as f:
plistlib.dump(settings, f) # O(n)
with path.open('rb') as f:
assert plistlib.load(f) == {'version': 2, 'recent': ['/tmp/report.txt']}
Performance Best Practices¶
✅ Do:
- Use
dump()to a file for large XML plists: it streams, wheredumps()holds the whole document - Pass
sort_keys=Falsewhen key order does not matter, to skip an O(d log d) sort per dictionary - Use the binary format for values that repeat strings or data: equal ones are stored once
- Pass
fmt=FMT_XMLwhen reading XML from a stream that cannot seek; binary needs a seekable one
❌ Avoid:
- Expecting the binary writer to stream: it tables every object before writing, so it holds O(m) even when writing to a file
- Relying on sub-second dates in XML plists; they are truncated to whole seconds
Version Notes¶
- Python 3.13.16+ on the 3.13 line, and 3.14.8+ on the 3.14 line:
skipkeys=Trueskips non-string keys before sorting. Earlier versions, and 3.15.0, can raiseTypeErrorwhen sorting mixed key types; passsort_keys=Falsefor compatibility - Python 3.13+:
loads()accepts astrholding XML;load(),loads(),dump()anddumps()takeaware_datetime - All Python 3:
tupleandbytearrayare written as arrays and data, and read back aslistandbytes
Related Modules¶
- json - The same tree of dicts, lists and scalars, as text; no dates, bytes or UIDs
- pickle - Serializes arbitrary Python objects, not a portable format
- xml.etree.ElementTree - For XML that is not a plist