Python’s plistlib module reads and writes Apple property list (.plist) files in XML and binary formats. import plistlib. load: with open("Info.plist", "rb") as f: data = plistlib.load(f) — auto-detects XML or binary. loads: plistlib.loads(bytes_data) — parse from bytes. dump: with open("out.plist", "wb") as f: plistlib.dump(obj, f, fmt=plistlib.FMT_XML). dumps: plistlib.dumps(obj, fmt=plistlib.FMT_BINARY) → bytes. FMT_XML: produces <?xml version="1.0"...><plist version="1.0">...</plist> — human-readable. FMT_BINARY: produces compact bplist00 binary — used by macOS Preferences subsystem, iOS bundles. Type mapping: dict → <dict>, list/tuple → <array>, str → <string>, int → <integer>, float → <real>, bool → <true/false>, bytes/bytearray → <data>, datetime.datetime → <date>, plistlib.UID(n) → UID (NSKeyedArchiver). sort_keys: dumps(obj, sort_keys=True) — stable XML output. skipkeys=False (default, raises on non-string keys). InvalidFileException raised for corrupt files. Claude Code generates macOS app config readers, Info.plist bundlers, iOS preferences file parsers, and NSKeyedArchiver UID utilities.
CLAUDE.md for plistlib
## plistlib Stack
- Stdlib: import plistlib
- Read: with open("Info.plist", "rb") as f: data = plistlib.load(f)
- Parse: data = plistlib.loads(raw_bytes)
- Write: plistlib.dump(obj, f, fmt=plistlib.FMT_XML)
- Bytes: xml_bytes = plistlib.dumps(obj, fmt=plistlib.FMT_XML)
bin_bytes = plistlib.dumps(obj, fmt=plistlib.FMT_BINARY)
- UID: plistlib.UID(42) # for NSKeyedArchiver plists
plistlib Property List Pipeline
# app/plistutil.py — read, write, convert, merge, validate, diff
from __future__ import annotations
import datetime
import plistlib
from dataclasses import dataclass
from pathlib import Path
from typing import Any
# ─────────────────────────────────────────────────────────────────────────────
# 1. Read helpers
# ─────────────────────────────────────────────────────────────────────────────
def read_plist(path: str | Path) -> Any:
"""
Read a plist file (XML or binary, auto-detected).
Example:
data = read_plist("Info.plist")
print(data["CFBundleIdentifier"])
"""
with Path(path).open("rb") as f:
return plistlib.load(f)
def parse_plist(data: bytes) -> Any:
"""
Parse plist bytes (XML or binary, auto-detected).
Example:
obj = parse_plist(response.content)
"""
return plistlib.loads(data)
def read_plist_safe(path: str | Path, default: Any = None) -> Any:
"""
Read a plist file, returning default on error.
Example:
prefs = read_plist_safe("~/.config/app.plist", {})
"""
try:
return read_plist(path)
except (plistlib.InvalidFileException, OSError, ValueError):
return default
# ─────────────────────────────────────────────────────────────────────────────
# 2. Write helpers
# ─────────────────────────────────────────────────────────────────────────────
def write_plist(
obj: Any,
path: str | Path,
fmt: int = plistlib.FMT_XML,
sort_keys: bool = True,
) -> None:
"""
Write a Python object to a plist file.
fmt: plistlib.FMT_XML (default) or plistlib.FMT_BINARY
sort_keys: produce stable XML output (default True).
Example:
write_plist({"CFBundleIdentifier": "com.example.app"}, "Info.plist")
write_plist({"token": "abc"}, "prefs.plist", fmt=plistlib.FMT_BINARY)
"""
with Path(path).open("wb") as f:
plistlib.dump(obj, f, fmt=fmt, sort_keys=sort_keys)
def to_xml_bytes(obj: Any, sort_keys: bool = True) -> bytes:
"""
Serialize to XML plist bytes.
Example:
xml = to_xml_bytes({"key": "value"})
print(xml.decode())
"""
return plistlib.dumps(obj, fmt=plistlib.FMT_XML, sort_keys=sort_keys)
def to_binary_bytes(obj: Any) -> bytes:
"""
Serialize to binary bplist00 bytes.
Example:
blob = to_binary_bytes({"token": "secret", "ts": 1234567890})
"""
return plistlib.dumps(obj, fmt=plistlib.FMT_BINARY)
def convert_plist(
src: str | Path,
dst: str | Path,
fmt: int = plistlib.FMT_BINARY,
) -> None:
"""
Convert a plist file between XML and binary formats.
Example:
convert_plist("prefs.xml.plist", "prefs.binary.plist", plistlib.FMT_BINARY)
convert_plist("archive.bplist", "archive.xml.plist", plistlib.FMT_XML)
"""
obj = read_plist(src)
write_plist(obj, dst, fmt=fmt)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Merge and update helpers
# ─────────────────────────────────────────────────────────────────────────────
def merge_plists(*sources: str | Path, output: str | Path,
fmt: int = plistlib.FMT_XML) -> dict:
"""
Deep-merge multiple plist files (left-wins on key collision).
Returns the merged dict and optionally writes it to output.
Example:
merged = merge_plists("base.plist", "override.plist", output="merged.plist")
"""
result: dict = {}
for src in sources:
data = read_plist(src)
if isinstance(data, dict):
result.update(data)
write_plist(result, output, fmt=fmt)
return result
def plist_set(path: str | Path, key: str, value: Any,
fmt: int | None = None) -> None:
"""
Set or update a single key in a plist file (creates if missing).
Preserves existing format unless fmt is specified.
Example:
plist_set("prefs.plist", "DarkMode", True)
plist_set("config.plist", "MaxRetries", 5)
"""
p = Path(path)
if p.exists():
data = read_plist(p)
if not isinstance(data, dict):
data = {}
else:
data = {}
data[key] = value
# Detect format from existing file if not specified
if fmt is None:
fmt = _detect_fmt(p)
write_plist(data, p, fmt=fmt)
def _detect_fmt(path: Path) -> int:
"""Detect whether an existing plist is binary or XML."""
try:
with path.open("rb") as f:
magic = f.read(8)
if magic.startswith(b"bplist"):
return plistlib.FMT_BINARY
except OSError:
pass
return plistlib.FMT_XML
def plist_get(path: str | Path, key: str, default: Any = None) -> Any:
"""
Read a single key from a plist file.
Example:
debug_mode = plist_get("config.plist", "Debug", False)
"""
data = read_plist_safe(path, {})
if isinstance(data, dict):
return data.get(key, default)
return default
# ─────────────────────────────────────────────────────────────────────────────
# 4. Validation and diff
# ─────────────────────────────────────────────────────────────────────────────
_PLIST_VALID_TYPES = (
dict, list, tuple, str, int, float, bool,
bytes, bytearray, datetime.datetime, plistlib.UID,
)
def validate_plist_types(obj: Any, path: str = "") -> list[str]:
"""
Walk obj and return error strings for any value whose type is not
supported by plistlib. Returns [] if the object is fully valid.
Example:
errors = validate_plist_types({"key": {1: "bad_int_key"}})
for e in errors:
print(e)
"""
errors: list[str] = []
_validate_recursive(obj, path, errors)
return errors
def _validate_recursive(obj: Any, path: str, errors: list[str]) -> None:
if isinstance(obj, dict):
for k, v in obj.items():
if not isinstance(k, str):
errors.append(f"{path}: dict key {k!r} is not a str")
_validate_recursive(v, f"{path}.{k}" if path else k, errors)
elif isinstance(obj, (list, tuple)):
for i, v in enumerate(obj):
_validate_recursive(v, f"{path}[{i}]", errors)
elif not isinstance(obj, _PLIST_VALID_TYPES):
errors.append(f"{path}: unsupported type {type(obj).__name__!r} ({obj!r})")
@dataclass
class PlistDiffEntry:
key_path: str
left: Any
right: Any
def __str__(self) -> str:
return f" {self.key_path}: {self.left!r} → {self.right!r}"
def diff_plists(a: Any, b: Any, path: str = "") -> list[PlistDiffEntry]:
"""
Recursively diff two plist objects (typically dicts).
Returns a list of differing entries.
Example:
diffs = diff_plists(read_plist("old.plist"), read_plist("new.plist"))
for d in diffs:
print(d)
"""
diffs: list[PlistDiffEntry] = []
_diff_recursive(a, b, path, diffs)
return diffs
def _diff_recursive(a: Any, b: Any, path: str, out: list[PlistDiffEntry]) -> None:
if type(a) != type(b):
out.append(PlistDiffEntry(path, a, b))
return
if isinstance(a, dict):
all_keys = set(a) | set(b)
for k in sorted(all_keys):
child = f"{path}.{k}" if path else k
if k not in a:
out.append(PlistDiffEntry(child, None, b[k]))
elif k not in b:
out.append(PlistDiffEntry(child, a[k], None))
else:
_diff_recursive(a[k], b[k], child, out)
elif isinstance(a, (list, tuple)):
for i, (av, bv) in enumerate(zip(a, b)):
_diff_recursive(av, bv, f"{path}[{i}]", out)
if len(a) != len(b):
out.append(PlistDiffEntry(f"{path}[len]", len(a), len(b)))
elif a != b:
out.append(PlistDiffEntry(path, a, b))
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import tempfile
print("=== plistlib demo ===")
info_plist_obj = {
"CFBundleIdentifier": "com.example.myapp",
"CFBundleVersion": "1.0.0",
"CFBundleDisplayName": "My App",
"LSMinimumSystemVersion": "13.0",
"NSHighResolutionCapable": True,
"BuildDate": datetime.datetime(2028, 9, 27, 12, 0, 0),
"Frameworks": ["Foundation", "AppKit"],
"Metadata": {"Author": "Claude", "License": "MIT"},
}
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
# ── write XML and binary ───────────────────────────────────────────────
print("\n--- write XML and binary plists ---")
xml_path = tmp / "Info.plist"
bin_path = tmp / "Info.binary.plist"
write_plist(info_plist_obj, xml_path, fmt=plistlib.FMT_XML)
write_plist(info_plist_obj, bin_path, fmt=plistlib.FMT_BINARY)
print(f" XML: {xml_path.stat().st_size:,d} bytes")
print(f" Binary: {bin_path.stat().st_size:,d} bytes")
# ── read back ──────────────────────────────────────────────────────────
print("\n--- read_plist ---")
loaded = read_plist(xml_path)
print(f" CFBundleIdentifier: {loaded['CFBundleIdentifier']!r}")
print(f" BuildDate: {loaded['BuildDate']}")
print(f" Frameworks: {loaded['Frameworks']}")
# ── to_xml_bytes (inspect first 120 chars) ─────────────────────────────
print("\n--- to_xml_bytes snippet ---")
xml_bytes = to_xml_bytes({"key": "value", "count": 42})
print(f" {xml_bytes[:120].decode()!r}")
# ── plist_get / plist_set ──────────────────────────────────────────────
print("\n--- plist_get / plist_set ---")
print(f" CFBundleVersion: {plist_get(xml_path, 'CFBundleVersion')!r}")
plist_set(xml_path, "Debug", True)
print(f" after set Debug: {plist_get(xml_path, 'Debug')!r}")
# ── convert format ─────────────────────────────────────────────────────
print("\n--- convert_plist ---")
converted = tmp / "converted.plist"
convert_plist(xml_path, converted, plistlib.FMT_BINARY)
data = read_plist(converted)
print(f" converted has CFBundleIdentifier: {data['CFBundleIdentifier']!r}")
# ── validate ───────────────────────────────────────────────────────────
print("\n--- validate_plist_types ---")
good_errors = validate_plist_types(info_plist_obj)
bad_obj = {"ok": "yes", "bad_key": {1: "int key"}, "set_val": {1, 2}}
bad_errors = validate_plist_types(bad_obj)
print(f" valid object errors: {good_errors}")
print(f" invalid object errors:")
for e in bad_errors:
print(f" {e}")
# ── diff ───────────────────────────────────────────────────────────────
print("\n--- diff_plists ---")
v1 = {"CFBundleVersion": "1.0.0", "Debug": False, "MaxRetries": 3}
v2 = {"CFBundleVersion": "1.1.0", "Debug": False, "MaxRetries": 5, "NewKey": "hello"}
diffs = diff_plists(v1, v2)
for d in diffs:
print(d)
# ── UID type ───────────────────────────────────────────────────────────
print("\n--- UID ---")
uid_obj = {"$uid": plistlib.UID(42)}
uid_bytes = to_binary_bytes(uid_obj)
back = parse_plist(uid_bytes)
print(f" UID round-trip: {back['$uid']} (type={type(back['$uid']).__name__})")
print("\n=== done ===")
For the xml.etree.ElementTree alternative — xml.etree.ElementTree (stdlib) can parse plist XML directly as generic XML and lets you walk the tree manually, giving full control over handling malformed or extended plist dialects — use ElementTree when you need to process non-standard plist XML that plistlib rejects, or when you’re building a plist parser for a target format that extends Apple’s schema; use plistlib for standard Apple plist files because it handles both XML and binary formats, maps plist types to Python types automatically, and is much more concise. For the biplist / plistlib (C extension) alternative — biplist (PyPI) is a third-party pure-Python binary plist reader/writer that handles edge cases in binary plists (UID arrays, offset table sizes >4 bytes) that older versions of plistlib could not; since Python 3.4 plistlib handles all standard binary plist features natively — use plistlib for all standard macOS/iOS plists; fall back to biplist only if you encounter binary plists from unusual sources that fail to parse with plistlib.InvalidFileException. The Claude Skills 360 bundle includes plistlib skill sets covering read_plist()/parse_plist()/read_plist_safe() readers, write_plist()/to_xml_bytes()/to_binary_bytes()/convert_plist() writers, merge_plists()/plist_set()/plist_get() dict update helpers, validate_plist_types() type checker, and PlistDiffEntry dataclass with diff_plists() recursive differ. Start with the free tier to try property list patterns and plistlib pipeline code generation.