Quick Tip: Controlling macOS with Python

    Stuart Langridge

    In this quick tip, excerpted from Useful Python, Stuart looks at ways to control the Windows OS with Python.

    Working on a Mac, we can control almost everything about the system using pyobjc, the Python-to-Objective-C bridge. Apple makes most of its OS controllable via the AppKit module, and pyobjc gives Python access to all of this. This will be most useful if we already know the AppKit way to do the thing we want, but with a little exploration it’s possible to make our way through the operating system APIs.

    Let’s try an example. First, we’ll need pyobjc, which can be installed with pip install pyobjc. This will install a whole list of operating system API bridges, allowing access to all sorts of aspects of macOS. For now, we’ll consider AppKit, which is the tool used to build and control running apps on a Mac desktop.

    We can list all the applications currently running using AppKit:

    Python 3.9.6 (default, Oct 18 2022, 12:41:40) 
    [Clang 14.0.0 (clang-1400.0.29.202)] on darwin
    Type "help", "copyright", "credits" or "license" for more information.
    >>> from AppKit import NSWorkspace
    >>> NSWorkspace.sharedWorkspace().runningApplications() 
        "<NSRunningApplication: 0x60000145c000 (com.apple.loginwindow - 148) LSASN:{hi=0x0;lo=0x6006}>",
        "<NSRunningApplication: 0x60000145c080 (com.apple.backgroundtaskmanagement.agent - 475) LSASN:{hi=0x0;lo=0xb00b}>",
        "<NSRunningApplication: 0x60000145c100 (com.apple.WindowManager - 474) LSASN:{hi=0x0;lo=0xc00c}>",
        "<NSRunningApplication: 0x60000145c180 (com.apple.CoreLocationAgent - 500) LSASN:{hi=0x0;lo=0xe00e}>",
        "<NSRunningApplication: 0x60000145c980 (com.apple.Terminal - 1302) LSASN:{hi=0x0;lo=0x24024}>",
        "<NSRunningApplication: 0x60000145ca00 (com.apple.Safari - 1303) LSASN:{hi=0x0;lo=0x25025}>",
        "<NSRunningApplication: 0x60000145cb80 (com.apple.Spotlight - 1310) LSASN:{hi=0x0;lo=0x28028}>",
        "<NSRunningApplication: 0x60000145cc00 (com.apple.finder - 1306) LSASN:{hi=0x0;lo=0x29029}>",

    This will give a long list of NSRunningApplication objects. Each one corresponds to a specific application currently running on the desktop. Many are “invisible” applications (things that are running but aren’t necessarily showing a window), but others are things that we might think of as actual applications that we can see—such as Safari, Terminal, and so on. NSRunningApplication is documented at developer.apple.com, where its properties can be seen. For example, each application has a localizedName and a bundleIdentifier:

    >>> for nsapp in NSWorkspace.sharedWorkspace().runningApplications():
    ...   print(f"{nsapp.localizedName()} -> {nsapp.bundleIdentifier()}")
    loginwindow -> com.apple.loginwindow
    BackgroundTaskManagementAgent -> com.apple.backgroundtaskmanagement.agent
    WindowManager -> com.apple.WindowManager
    CoreLocationAgent -> com.apple.CoreLocationAgent
    Terminal -> com.apple.Terminal
    Safari -> com.apple.Safari
    Spotlight -> com.apple.Spotlight
    Finder -> com.apple.finder

    We can also see that a NSRunningApplication object has an activate function, which we can call to activate that app as though we had clicked its icon in the Dock. So, to find Safari and then activate it, we would use that activate function. The call to activate requires a value for options, as the documentation describes, and that also needs to be imported from AppKit:

    >>> from AppKit import NSWorkspace, NSApplicationActivateIgnoringOtherApps
    >>> safari_list = [x for x in NSWorkspace.sharedWorkspace().runningApplications()
        if x.bundleIdentifier() == 'com.apple.Safari']
    >>> safari = safari_list[0]
    >>> safari.activateWithOptions_(NSApplicationActivateIgnoringOtherApps)

    Now Safari is activated.

    Finding Python Versions of macOS APIs

    Finding the name of something in Python that corresponds to the Objective-C name can be a little tricky. As shown in the code above, the Objective-C activate function is called activateWithOptions_ in Python. There’s a set of rules for this name translation, which the pyobjc documentation explains, but it can sometimes be quicker to use Python’s own dir() function to show all the properties of an object and then pick out the one that looks most plausible:

    >>> print(len(dir(safari)))

    Ouch! Our safari instance of an NSRunningApplication has 452 properties! Well, the one we want is probably called something like “activate”, so:

    >>> print([x for x in dir(safari) if "activate" in x.lower()])
    ['activateWithOptions_', 'activateWithOptions_']

    Aha! So activateWithOptions_ is the name of the function we need to call. Similarly, the name of the option we want to pass to that function is in AppKit itself:

    >>> [x for x in dir(AppKit) if "ignoringotherapps" in x.lower()]

    This process can feel a little exploratory at times, but it’s possible to do anything that Objective-C can do from Python as well.

    This article is excerpted from Useful Python, available on SitePoint Premium and from ebook retailers.