Quick Tip: Controlling macOS with Python

Share this article

Quick Tip: Controlling macOS with Python

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)))
452

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()]
['NSApplicationActivateIgnoringOtherApps']

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.

Frequently Asked Questions (FAQs) about Controlling MacOS with Python

What is AppKit and how is it used in Python for MacOS control?

AppKit is a framework in the macOS SDK that contains all the objects necessary for implementing a graphical, event-driven user interface in a macOS application. It provides a wide range of classes and functions for creating and managing application windows, handling user input, drawing graphics, and performing other tasks related to the user interface. In Python, you can use the PyObjC bridge to access AppKit and other Objective-C frameworks. This allows you to write Python scripts that can control macOS applications, manipulate windows, and interact with system services.

How can I install the PyObjC module in Python?

PyObjC is a Python to Objective-C bridge that allows you to write full-featured macOS applications in Python. You can install it using pip, the Python package installer. Open a terminal window and type the following command: pip install pyobjc. This will download and install the PyObjC module and its dependencies. Once the installation is complete, you can import the module in your Python scripts with import objc.

I’m getting a “No module named AppKit” error. What should I do?

This error usually means that the AppKit module is not installed or not found in your Python environment. First, make sure that you have installed the PyObjC module, which includes AppKit. If you have installed PyObjC but still get the error, it’s possible that you’re using a different Python environment where PyObjC is not installed. In this case, you need to install PyObjC in the correct Python environment, or switch to the Python environment where PyObjC is installed.

How can I control macOS applications with Python?

With the PyObjC bridge, you can use Python to control macOS applications by sending AppleScript commands or using the scripting bridge. For example, you can launch applications, manipulate windows, send keystrokes, and perform other tasks. This requires a good understanding of both Python and AppleScript, as well as the application’s scripting interface.

How can I use Python to manipulate windows in macOS?

The AppKit framework provides several classes for working with windows, such as NSWindow and NSApplication. You can use these classes to get a list of all open windows, bring a window to the front, resize or move a window, and perform other window-related tasks. This requires using the PyObjC bridge to access the AppKit classes from Python.

Can I use Python to interact with system services in macOS?

Yes, you can use Python and the PyObjC bridge to interact with various system services in macOS. For example, you can use the NSWorkspace class to open URLs, launch applications, and perform other tasks related to the user’s workspace. You can also use the NSNotificationCenter class to post and observe notifications, which allows your script to respond to system events.

How can I send keystrokes from a Python script in macOS?

You can use the AppKit framework’s NSEvent class to create and post keyboard events, which effectively allows you to send keystrokes from a Python script. This requires a good understanding of the NSEvent class and the keyboard event types, as well as the key codes for the keys you want to press.

Can I use Python to draw graphics in macOS?

Yes, the AppKit framework provides several classes for drawing graphics, such as NSGraphicsContext, NSBezierPath, and NSColor. You can use these classes to draw lines, shapes, and images, set the drawing color, and perform other drawing tasks. This requires using the PyObjC bridge to access the AppKit classes from Python.

How can I handle user input in a Python script for macOS?

The AppKit framework provides several classes for handling user input, such as NSEvent and NSResponder. You can use these classes to get mouse events, keyboard events, and other types of user input. This requires using the PyObjC bridge to access the AppKit classes from Python.

Can I write a full-featured macOS application in Python?

Yes, with the PyObjC bridge, you can write a full-featured macOS application in Python. This includes creating a graphical user interface with windows, buttons, and other controls, handling user input, drawing graphics, and interacting with system services. However, this requires a good understanding of both Python and the macOS SDK, as well as the AppKit framework and other Objective-C frameworks.

Stuart LangridgeStuart Langridge
View Author

Stuart is a consultant CTO, software architect, and developer to startups and small firms on strategy, custom development, and how to best work with the dev team. Code and writings are to be found at kryogenix.org and social networks; Stuart himself is mostly to be found playing D&D or looking for the best vodka Collins in town.

macmac os xpythonPython Quick Tips
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week