Applications and tools in the Apple ecosystem use property list files to store configuration, metadata, and simple structured values. Python plistlib reads and writes both XML and binary plists without requiring macOS, making it useful for build automation, preference analysis, configuration generation, and interoperability with iOS and macOS tooling.
The format supports dictionaries, lists, strings, numbers, booleans, bytes, and dates, but it is not an arbitrary Python-object serializer. Dictionary keys must be strings, and unsupported values raise errors. This guide explains format selection, timezone-aware dates, UIDs, schema validation, external-input limits, semantic comparison, and atomic file replacement.
It complements our guides to Python netrc, tempfile, filecmp, importlib.resources, and copy.
What a property list is
A plist represents a tree of simple values. The root is usually a dictionary, although lists and other supported types can also appear. Apple software uses the format for preferences, manifests, application metadata, and tooling output.
There are two main variants: XML, which is readable and easy to review, and binary, which is more compact and can be faster for some workloads.
Reading a plist file
import plistlib
with open("config.plist", "rb") as file:
data = plistlib.load(file)
print(data)Open the file in binary mode. With the default fmt=None, the module automatically detects XML or binary content.
Reading in-memory data
content = path.read_bytes()
data = plistlib.loads(content)loads() is useful for HTTP responses, zip entries, packaged resources, database blobs, and test fixtures. The input is normally bytes; since Python 3.13, a string is accepted when XML format is explicitly selected.
Writing XML
config = {
"Name": "My Application",
"Version": 3,
"Enabled": True,
"Features": ["sync", "backup"],
}
with open("config.plist", "wb") as file:
plistlib.dump(config, file, fmt=plistlib.FMT_XML)XML is convenient in repositories, code review, and support diagnostics. Large XML plists, however, can be substantially bigger than binary equivalents.
Writing binary plists
with open("config-binary.plist", "wb") as file:
plistlib.dump(
config,
file,
fmt=plistlib.FMT_BINARY,
)Binary format preserves the same basic model but is not intended for manual editing. Use an appropriate inspection tool when debugging.
Serializing to bytes
xml_data = plistlib.dumps(config, fmt=plistlib.FMT_XML)
binary_data = plistlib.dumps(config, fmt=plistlib.FMT_BINARY)This API works well in tests, network services, and libraries that accept byte buffers rather than files.
Supported types
The module supports strings, integers, floats, booleans, tuples, lists, dictionaries with string keys, bytes, bytearray, and datetime values. Containers can combine these recursively.
Custom objects, sets, Decimal values, paths, and database connections are not converted automatically. Transform them into an explicit data model first.
Dictionary keys must be strings
data = {1: "value"}
plistlib.dumps(data) # TypeErrorWith skipkeys=True, invalid keys are silently omitted. That can destroy information, so the default False is safer. Validate and intentionally map keys before serialization.
Key ordering
sort_keys=True is the default and creates stable dictionary ordering in output.
plistlib.dumps(data, sort_keys=False)Stable XML reduces noisy diffs. Disable sorting only when insertion order has documented value for human workflows. Consumers should not depend on ordering unless their contract explicitly requires it.
Dates and time zones
Property lists represent dates as UTC instants. Since Python 3.13, aware_datetime=True can return values with tzinfo=datetime.UTC and convert aware values to UTC during writing.
from datetime import datetime, UTC
config = {"GeneratedAt": datetime.now(UTC)}
content = plistlib.dumps(
config,
aware_datetime=True,
)Avoid naive datetimes for real-world instants. Define one timezone policy and test conversion around daylight-saving transitions.
Loading aware datetimes
data = plistlib.loads(
content,
aware_datetime=True,
)
print(data["GeneratedAt"].tzinfo)Without this option, traditional behavior returns naive values. Mixing aware and naive values causes comparison failures or incorrect assumptions.
Binary UID tokens
plistlib.UID represents identifiers used in NSKeyedArchiver data.
uid = plistlib.UID(42)
print(uid.data)The value must be between zero and 2**64 - 1. A UID is not an automatically resolved Python object reference; interpreting an archive requires understanding the producer’s object table.
NSKeyedArchiver is not a normal dictionary
Archived object graphs use tables and UID references. Parsing the plist is only the first step. Do not assume fields map directly to final application objects.
For unknown archives, impose size, depth, object-count, and processing-time limits.
Invalid files
Unparseable content raises plistlib.InvalidFileException. Malformed XML can also surface parser exceptions.
try:
data = plistlib.loads(content)
except (plistlib.InvalidFileException, ValueError) as error:
raise RuntimeError("invalid plist") from errorDo not place the entire untrusted document in an error message; it may be sensitive or extremely large.
Integer limits
Binary plists have representable integer limits. Values outside those limits raise OverflowError.
Validate domain ranges before serialization rather than depending only on a low-level exception.
XML security
The XML implementation uses Expat. External plists still require size limits and application-level validation. Never interpret internal strings as paths, commands, templates, or executable code without separate checks.
Unknown XML elements can be ignored by the plist parser, so successful parsing is not proof that the document follows your schema.
Validating the application model
def validate_config(data):
if not isinstance(data, dict):
raise TypeError("root must be a dictionary")
if not isinstance(data.get("Name"), str):
raise ValueError("Name is missing or invalid")
if data.get("Version", 0) < 1:
raise ValueError("Version is invalid")Plistlib validates the serialization format, not business requirements. Check required fields, types, ranges, enumerations, and cross-field relationships.
Atomic writing
Do not overwrite critical configuration directly. Create a temporary file in the same directory, set permissions, serialize, flush, and replace the destination.
from pathlib import Path
import tempfile
import os
final = Path("config.plist")
with tempfile.NamedTemporaryFile(
dir=final.parent,
delete=False,
) as tmp:
plistlib.dump(config, tmp)
tmp.flush()
os.fsync(tmp.fileno())
temporary = Path(tmp.name)
temporary.replace(final)Permissions and ownership
Replacing a file can change permissions, ACLs, extended attributes, or ownership. System configuration tools should preserve and reapply required metadata or use a deployment mechanism designed for that environment.
Semantic comparison
Byte comparison can report differences caused only by XML whitespace, dictionary order, or format choice. For semantic comparison, parse both documents and compare the resulting structures.
a = plistlib.loads(file_a)
b = plistlib.loads(file_b)
assert a == bNormalize aware and naive datetimes before comparison.
Converting XML to binary
with open("input.plist", "rb") as source:
data = plistlib.load(source)
with open("output.plist", "wb") as target:
plistlib.dump(data, target, fmt=plistlib.FMT_BINARY)Validate between reading and writing. Conversion is not a security sanitizer for an untrusted document.
Packaged templates
A plist template distributed with a Python package can be read with importlib.resources, modified in memory, and written to a user or application configuration directory.
Do not write back into an installed package. Treat packaged resources as read-only.
Testing
Create round-trip tests for XML and binary formats, Unicode, bytes, empty collections, dates, extreme integers, invalid keys, and malformed input.
def test_round_trip():
original = {"Name": "Café", "Enabled": True}
for format_ in (plistlib.FMT_XML, plistlib.FMT_BINARY):
restored = plistlib.loads(
plistlib.dumps(original, fmt=format_)
)
assert restored == originalCompatibility testing
If an older macOS, iOS, Swift, Objective-C, or third-party tool consumes the file, test the final artifact on that platform. Not every implementation treats extensions or specialized structures identically.
Common mistakes
- Opening files in text mode.
- Using non-string dictionary keys.
- Enabling
skipkeysand losing fields. - Mixing aware and naive datetimes.
- Treating a UID as a resolved object.
- Trusting a file merely because parsing succeeded.
- Overwriting critical configuration directly.
- Comparing bytes when the goal is data equality.
Best practices
- Open files in binary mode.
- Validate the model after loading.
- Use
aware_datetime=Truefor real instants. - Choose XML for review and binary only for a reason.
- Write atomically.
- Limit external input size and depth.
- Test both formats used by the application.
- Never execute strings extracted from a plist.
Conclusion
Python plistlib supports XML and binary property lists, including bytes, dates, and UID tokens. It is a practical interoperability tool for the Apple ecosystem and cross-platform automation.
The parser validates the file format, not your application rules. Combine it with resource limits, schema checks, a timezone policy, and atomic writes. Consult the official plistlib documentation and Apple’s property list documentation for compatibility details.







