Python plistlib: Apple Plist Files

Published on: August 8, 2026
Reading time: 6 minutes
Configuration icon representing plist files with Python plistlib

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)  # TypeError

With 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 error

Do 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 == b

Normalize 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 == original

Compatibility 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 skipkeys and 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=True for 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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Digital padlock representing host credentials with Python netrc
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python netrc: Credentials by Host

    Learn Python netrc to read credentials by host, validate permissions, handle parse errors, and integrate network clients safely.

    Ler mais

    Tempo de leitura: 6 minutos
    08/08/2026
    Digital message representing quoted-printable encoding with Python quopri
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python quopri: Quoted-Printable

    Learn Python quopri to encode and decode quoted-printable data in email, files, and MIME integrations safely.

    Ler mais

    Tempo de leitura: 6 minutos
    08/08/2026
    Digital file icon representing MIME types with Python mimetypes
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python mimetypes: MIME Types

    Learn Python mimetypes to identify MIME types, extensions, and encodings safely in uploads, downloads, email, and web APIs.

    Ler mais

    Tempo de leitura: 6 minutos
    08/08/2026
    Binary search and sorted lists with Python bisect
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python bisect: Search Sorted Lists

    Learn Python bisect for binary search, ordered insertion, duplicate ranges, thresholds, and safe sorted-list design.

    Ler mais

    Tempo de leitura: 4 minutos
    08/08/2026
    Code and packaged files with Python importlib.resources
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python importlib.resources: Practical Guide

    Learn Python importlib.resources to access packaged files safely across wheels, installed applications, and custom import loaders.

    Ler mais

    Tempo de leitura: 5 minutos
    07/08/2026
    Keyboard and data flow representing multiple-file processing with Python fileinput
    Libraries and Modules
    Foto de perfil de Leandro Hirt da Academify

    Python fileinput: Read Multiple Files

    Learn Python fileinput to read multiple files or stdin, track line numbers, open compressed logs, and rewrite content safely with

    Ler mais

    Tempo de leitura: 7 minutos
    07/08/2026