Introduction

Python is ubiquitous. There are so many open-source modules out there that embedding an interpreter in your app may allow you to incorporate sophisticated features with very little new coding.

Embedding Python has never been trivial, but Apple’s obsession with security, and its shift from Intel to Apple Silicon, has added another level of complexity to an already cumbersome process.

There are two pieces of open source software out there that promise to make this easy: [PythonKit](<https://github.com/pvieito/PythonKit>), and [Python_Apple_Support](<https://github.com/beeware/Python-Apple-support>). I was at first unsuccessful when following the provided instructions, and it took me quite a lot of experimentation and trial and error to discover the correct procedure.

In this tutorial, I delineate the exact steps I took to successfully embedded the Python interpreter in a Swift app using Xcode.

Steps

  1. Install the Python-Apple-Support binaries in your Xcode project.

    First download the pre-built binaries using the following links:

    for macOS: https://github.com/beeware/Python-Apple-support/releases/download/3.11-b1/Python-3.11-macOS-support.b1.tar.gz

    for iOS: https://github.com/beeware/Python-Apple-support/releases/download/3.11-b1/Python-3.11-iOS-support.b1.tar.gz

    Untar the downloaded file

    tar -xvf Python-3.11-macOS-support.b1.tar.gz
    

    Navigate to the Python-3.11-macOS-support.b1 folder and drag python-stdlib and Python.xcframework folders into your project

    Untitled

    Check Copy items if needed before clicking Finish.

    Untitled

  2. Add the PythonKit Package Dependency.

    Untitled

    In the search field at the upper right, type in the PythonKit’s git repo address (https://github.com/pvieito/PythonKit.git), select PythonKit, and click Add Package.

    Untitled

  3. Verify Setting for Python.xcframework

    Click the root node of your project, and select your app’s target. In the General pane, scroll down to Frameworks, Libraries, and Embedded Content and ensure that Python.xcframework is set to Do Not Embed.

    Untitled

  4. Add the SystemConfiguration.Framework

    Untitled

    Untitled

    Make sure that it is also set Do Not Embed.

    Ensure that the python-stdlib folder will be copied as a bundle resource.

    Untitled

  5. Create a module map for the Python.xcframework

    Hit ⌘N and create a new blank file

    Untitled

    Name it module.modulemap, and save it in Python.xcframework/macos_arm64_x86_64/Headers

    Untitled

    And paste in the following.

    module Python {
     umbrella header "Python.h"
     export *
     link "Python"
    }
    
  6. Add a Run Script phase to codesign the PythonKit binaries

    Untitled

    Paste in the flowing script which signs the embedded Python binaries. (Do not use the script provided by the author. It is incorrect.)

    set -e
    echo "Signing contents of $CODESIGNING_FOLDER_PATH/Contents/Resources/python-stdlib/lib-dynload as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)"
    cd "$CODESIGNING_FOLDER_PATH/Contents/Resources/python-stdlib/lib-dynload"
    /usr/bin/codesign —-force —-sign "$EXPANDED_CODE_SIGN_IDENTITY" --options runtime —-timestamp=none —-preserve-metadata=identifier,entitlements,flags —-generate-entitlement-der *.so
    

    Uncheck Based on dependency analysis.

    Untitled

Swift/Python Interop

The Python runtime should be initialized as early in your app as possible. Below is some sample code illustrating how Python is accessed.

//
//  EmbeddedPythonApp.swift
//  EmbeddedPython
//
//  Created by Edward Janne on 1/8/23.
//

import SwiftUI
import Python
import PythonKit

class EmbeddedPython: ObservableObject {
    init() {
				// Initialize the Python runtime early in the app
        if  let stdLibPath = Bundle.main.path(forResource: "python-stdlib", ofType: nil),
            let libDynloadPath = Bundle.main.path(forResource: "python-stdlib/lib-dynload", ofType: nil)
        {
            setenv("PYTHONHOME", stdLibPath, 1)
            setenv("PYTHONPATH", "\\(stdLibPath):\\(libDynloadPath)", 1)
            Py_Initialize()
        }
    }
		
		// Simple example of loading Python modules into the runtime
		// and accessing Python object members in Swift
		func printPythonInfo() {
        let sys = Python.import("sys")
				
        print("Python Version: \\(sys.version_info.major).\\(sys.version_info.minor)")
        print("Python Encoding: \\(sys.getdefaultencoding().upper())")
        print("Python Path: \\(sys.path)")

        _ = Python.import("math") // verifies `lib-dynload` is found and signed successfully
		}
}

@main
struct EmbeddedPythonApp: App {
    
    @StateObject var embeddedPython = EmbeddedPython()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
								.onAppear {
										embeddedPython.printPythonInfo()
								}
			  }
    }
}