How to read from and write to .plist files using Python

Python and Macs

Python is a popular tool among Mac Admins. You can find a fairly comprehensive (and long) list of Python-based Mac Admin tools at Python Macadmin Tools, so it's a handy thing to be able to throw together a Python script every now and then.

.plist files

A lot of settings in Mac OS X are managed in property lists (or .plist files), so it's also a handy thing to be able to use a Python script to manipulate .plist files.

.plist files and Bash (Bourne Again Shell)

Generally, if you're using bash (the default shell when you open the Terminal.app), you would read from and write to .plist files using a defaults command. For example, if you wanted to see whether the last user logged in (loggedIn) is still logged in or if no one is logged in (loggedOut or Restart), you would use a command like this:

defaults read /Library/Preferences/com.apple.loginwindow lastUser
or if you wanted to show hidden files, you would change the relevant .plist using a command like this:
defaults write com.apple.Finder AppleShowAllFiles -bool TRUE

.plist files and Python... and plistlib

This is where things get a bit tricky, because there is no equivalent to defaults in Python. Python has a module you can import called plistlib that presumably lets you read from and write to .plist files.

If you follow the examples in the documentation, though, you may run into some errors.

For example, if you paste in the code on how to generate a .plist (even if you import datetime, plistlib, and time), you'll get an error of

NameError: name 'fileName' is not defined
and then if you actually define fileName with the path to a file, you'll get
NameError: name 'dump' is not defined
Now I get that probably dump needs to be imported from some other module, but seriously in documentation you need to have code that people can copy and paste and see the results of and tweak, instead of having code that's basically useless.

That's why I'm writing this guide, because it's difficult to find straightforward documentation on how to actually use plistlib.

This is an actual basic script that will actually write a .plist based on a dictionary you define:

#!/usr/bin/python

import os
import plistlib

def main():
   pl = {
   "aString" : "Doodah",
   "aList" : ["A", "B", 12, 32.1, [1, 2, 3]],
   "aFloat" : 0.1,
   "anInt" : 730
   }

   fileName=os.path.expanduser('~/Desktop/example.plist')

   plistlib.writePlist(pl, fileName)

if __name__ == '__main__':
   main()
and if you want to read a .plist, this is actual real code that will really work (obviously the script above and script below are just examples, and you would tweak them to fit your workflow):
#!/usr/bin/python

import os
import plistlib

def main():

   fileName=os.path.expanduser('~/Desktop/example.plist')
   
   if os.path.exists(fileName):

      pl=plistlib.readPlist(fileName)
   
      print '\nThe plist full contents is %s\n' % pl

      if 'aString' in pl:
         print 'The aString value is %s\n' % pl['aString']
      else:
         print 'There is no aString in the plist\n'

   else:
      print '%s does not exist, so can\'t be read' % fileName

if __name__ == '__main__':
   main()
You may run into an issue, though, with some .plist files, and you'll get an error message like this:
Traceback (most recent call last):
File "./NAMEOFYOURSCRIPT.py", line 26, in
main()
File "./NAMEOFYOURSCRIPT.py", line 13, in main
pl=plistlib.readPlist(fileName)
File
"/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plistlib.py", line 78, in readPlist
rootObject = p.parse(pathOrFile)
File
"/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/plistlib.py", line 406, in parse
parser.ParseFile(fileobj)
xml.parsers.expat.ExpatError: not well-formed (invalid token): line 1, column 8
I believe this stems from some .plist files being XML and others being binary. You can revert to bash and plutil to convert from binary to XML, but extra-converting a file in bash to be able to read from it in Python isn't ideal.

.plist files and Python... and FoundationPlist

Fortunately, there's a solution for this. FoundationPlist (by Greg Neagle, author of Munki) can read from and write to both XML and binary .plist files reliably.

To use FoundationPlist, just put it and its corresponding __init__.py in a FoundationPlist subfolder of your Python script, and then put

import FoundationPlist
in your Python script.

You can see a good example of this subfolder setup in Outset's code.

Once the FoundationPlist module is imported, you can use it similarly to how you would use plistlib:

munki_prefs_location='/Library/Preferences/ManagedInstalls.plist'
munki_prefs=FoundationPlist.readPlist(munki_prefs_location)
manifest=munki_prefs['ClientIdentifier']
print 'The client identifier is %s' % manifest
This is by no means a comprehensive guide to manipulating .plist files using Python—it's just a start, because I couldn't find good, usable documentation on the basics (read a .plist, write to a .plist).

PlistBuddy (not Python)

If you don't need to use Python and the defaults command isn't cutting it for you, Macs also come with a handy built-in command-line tool called PlistBuddy that can manipulate both binary and XML .plist files. I have several tutorials on how to use PlistBuddy.

1 thought on “How to read from and write to .plist files using Python”

Leave a Reply

Your email address will not be published. Required fields are marked *