# PythonKit: calling Python from Swift when a framework boundary needs crossing

> A Swift Package Manager library that embeds the CPython runtime, imports modules at runtime and lets you interleave Python and Swift calls. Small repository, narrow job, and a hard dependency on finding the right libpython on the machine you build on.

**pvieito/PythonKit** — Swift framework to interact with Python.

- Repository: https://github.com/pvieito/PythonKit
- Stars: 2,222 · Forks: 247
- Language: Swift
- License: Apache-2.0
- Published: 2026-10-08 · Updated: 2026-10-08 · Language: en
- Canonical page: https://hysenlabs.com/projects/pvieito-pythonkit

## One import line is the whole mental model

PythonKit's README opens with a Python snippet and then the same thing in Swift, and the second version is short enough to hold in your head:

```swift
import PythonKit

let sys = Python.import("sys")

print("Python \(sys.version_info.major).\(sys.version_info.minor)")
print("Python Version: \(sys.version)")
print("Python Encoding: \(sys.getdefaultencoding().upper())")
```

`Python.import` takes a module name as a string and hands back an object whose members are reachable as Swift properties and methods. `sys.version_info.major` becomes an attribute chain rather than a subscript, `sys.getdefaultencoding()` is called directly, and string interpolation uses ordinary Swift syntax with the value coming from Python. There is no bridging header, no generated wrapper code and no schema to describe your Python API first.

That last point is the real difference from the alternatives. Writing a CPython extension module means you describe the boundary in C, recompile for each platform and maintain that description as your Python API changes. PythonKit has no description step, which is why it stays small and why it also means there is nothing to catch a renamed attribute at build time. A typo in a Python method name surfaces when that line runs, not when the app compiles.

The Python side of the example is worth reading because it is what the Swift is mirroring: three prints from the standard `sys` module, one of which touches string encoding. Using the standard library as the example keeps the README honest, since `sys` needs no third-party install and no version pin.

## The library is found at runtime, not linked at build time

This is the detail that decides whether PythonKit works for you. Python libraries are loaded at runtime by PythonKit, which means the framework has to locate a working libpython on the machine where the binary actually runs. There is no compile-time link step that bakes in a path.

Because discovery happens at runtime, the search tries to find the most modern Python version available on the system, and the README documents three environment variables for overriding that behaviour. `PYTHON_VERSION` forces a specific major or minor version, `PYTHON_LIBRARY` takes a library path or name, and the examples show both spellings:

```console
$ PYTHON_VERSION=3 swift run
[*] Python 3.5
$ PYTHON_VERSION=2.7 swift run
[*] Python 2.7
$ PYTHON_LIBRARY=libpython3.5.so swift run
[*] Python 3.5
$ PYTHON_LIBRARY=/usr/lib/x86_64-linux-gnu/libpython2.7.so swift run
[*] Python 2.7
```

The examples are old. They show Python 2.7 and 3.5, which tells you the README has not been reworked in a while even though the repository itself is current. Treat the syntax as accurate and the version numbers as historical.

For a developer running on a laptop this is a non-issue: the loader finds something. For a build pipeline producing signed binaries for several platforms it is the thing that will consume your afternoon, and the two escape hatches are designed for it. `PYTHON_VERSION` handles the case where several Pythons are installed and you need one specific. `PYTHON_LIBRARY` handles the case where the library exists somewhere the loader would never guess.

## PYTHON_LOADER_LOGGING turns a fatal error into a directory listing

When PythonKit cannot find and load the Python library, you get a fatal error. When you set `PYTHON_LOADER_LOGGING`, you get the list of paths it tried first, which is the difference between a five minute fix and an afternoon of guessing:

```console
$ PYTHON_LOADER_LOGGING=TRUE PYTHON_VERSION=3.8 swift run
Loading symbol 'Py_Initialize' from the Python library...
Trying to load library at 'Python.framework/Versions/3.8/Python'...
Trying to load library at '/usr/local/Frameworks/Python.framework/Versions/3.8/Python'...
Fatal error: Python library not found. Set the PYTHON_LIBRARY environment variable with the path to a Python library.
```

Two things stand out in that trace. The loader is looking for a macOS framework bundle layout, with a relative `Python.framework/Versions/...` path first and an absolute `/usr/local/Frameworks/...` path second, which is the shape of a Homebrew or python.org install on Intel macOS. On Linux or inside a container the candidate list will be different, and the log is how you find out what it is actually looking for.

The first line also tells you what the loader is doing before it touches the filesystem: it loads the `Py_Initialize` symbol from the Python library. So a library that exists at the right path but lacks the expected symbols will fail differently from a library that is simply absent, and that distinction is worth reading for rather than skimming.

If you are writing a build script, turn this on unconditionally. It costs one environment variable and turns an opaque fatal error into a list of concrete paths.

## Hardened Runtime on macOS is the documented trap

The README's troubleshooting section has exactly one entry, and it is a good one. If you are targeting the Mac platform with the Hardened Runtime enabled, you must properly sign and embed the Python framework you are trying to load. Apple's Library Validation mechanism prevents a process from loading libraries that are not signed by Apple or by the same developer as the main process.

That constraint catches people building distributable Mac apps, which is precisely the audience most likely to want to reuse an existing Python package. The failure appears at runtime as a load error, and the fix is signing and embedding rather than any change to PythonKit itself. The README links Apple's documentation for both the Hardened Runtime and the Library Validation entitlement so you can see exactly what your binary is being asked to prove.

By contrast the troubleshooting section says nothing about Swift concurrency, sendability or actors. Given that the package targets modern Swift through Swift Package Manager, that silence means the library predates or deliberately avoids strict concurrency as a concern, and it means you should expect to do your own isolation work around Python objects. Python's global interpreter lock is real, and a bridge that hands you live Python objects into Swift code inherits the threading questions whether or not the library documents them.

## A deliberately small repository, with agent files in the tree

The tree is short enough to read in one look: `Package.swift`, `README.md`, `LICENSE.txt`, and two directories named `PythonKit/` and `Tests/`. There is no `docs/` folder, no `Examples/` folder and no CI configuration beyond `.github/`. For a library with 2,220 stars and 248 forks, that is a striking amount of surface area to have deliberately left out.

You should read that as a decision, not neglect. PythonKit's job is a binding, and the binding is small. Everything else you would look for, including how to install a Python package into the environment your app will use, belongs to the Python side of the boundary rather than to the Swift package. The absence of a `Package.resolved`-style pinning story in the tree is a related point: you are depending on a Python environment that Swift Package Manager knows nothing about.

Two things in the tree are unexpected and worth a sentence each. There are `AGENTS.md`, `CLAUDE.md`, a `Agents/` directory and a `.claude/` directory, which means the repository now carries instructions aimed at coding agents alongside its human documentation. There are also marker files named `.open_source` and `.public_project`. None of these affect how the library works, and all of them signal that the project is being maintained as a public, agent-assisted open source project rather than as an abandoned experiment.

The project history gives the origin story the README only mentions in passing. PythonKit was originally based on the `Python` module from the Swift for TensorFlow experimental project, so the code descends from a Google research effort that was retired when TensorFlow for Swift was. That lineage explains both the tight scope and the conservative documentation. Questions go to the Swift Forums, under related projects, which is where a library of this size and shape would be discussed.

## Adding it to a package, and where the docs run out

Installation is one line in your `Package.swift` manifest:

```swift
.package(url: "https://github.com/pvieito/PythonKit.git", from: "1.0.0"),
```

The `from:` clause starts at 1.0.0 and means Swift Package Manager resolves any 1.x release at or above it. Note that this is the only version information the README gives, and the repository has no published releases to browse, so the current 1.x version is something you discover through the resolver rather than by reading a tag list.

That absence is worth naming because it changes how you evaluate the project. There is no release notes to tell you whether a Swift version broke something, no migration guide, and no changelog in the tree. If you maintain this in production, you end up watching the commit log, and the last push on 2026-09-11 is the best available signal that the project is still being worked on.

The three open issues on the repository are a healthy number for a library this size, and they are the kind of number that suggests a small maintainer base with a low-traffic but stable user community rather than an abandoned project. The absence of any home page is consistent with that: the package lives on Swift Package Index, the README points there for its version and platform badges, and the Swift Forums are where the conversation happens.

So the honest summary of the documentation is that the README answers exactly three questions, how to import, how to choose a Python, and what to do when loading fails, and answers them well. Everything past that boundary is left to the Python documentation and to reading the source in `PythonKit/`.

## Conclusion

PythonKit solves one problem cleanly: Swift that needs a Python library, without shelling out or writing an extension module. The API surface is deliberately thin, the runtime lookup honours three environment variables you can actually control, and the repository carries almost no cruft. What it does not carry is documentation depth, a release feed or an example beyond the one in the README, so the questions a new user asks are the ones you will be answering yourself. If your use case fits, the fastest path is a single file that adds the package, imports `sys` and prints the version, then setting `PYTHON_LOADER_LOGGING=TRUE` the moment the library is not found.

## FAQ

### How does PythonKit let Swift code call a Python library?

PythonKit embeds the CPython runtime and exposes it through a Swift `Python` namespace. `Python.import("sys")` loads a module by name and returns an object whose attributes and methods are reachable as normal Swift properties and calls, so there is no generated wrapper and no bridging header to maintain when the Python API changes.

### Which Python version does PythonKit use, and can I choose it?

The Python library is loaded at runtime rather than linked at build time, and PythonKit searches for the most modern Python available on the system. Set the `PYTHON_VERSION` environment variable to force a particular version, or `PYTHON_LIBRARY` to give a full path or library name when the search cannot find the right one.

### Why does my build fail with 'Python library not found'?

The loader searched the locations it knows about and none contained a usable Python library. Setting `PYTHON_LOADER_LOGGING=TRUE` prints each candidate path it tries, along with the `Py_Initialize` symbol load, so you can see whether the library is absent or present but not loadable. The fix is usually pointing `PYTHON_LIBRARY` at the correct file.

### Does PythonKit work with the Hardened Runtime on macOS?

It can, but the Python framework has to be signed and embedded properly. Apple's Library Validation mechanism blocks a hardened process from loading libraries not signed by Apple or the same developer, so an unsigned Homebrew or python.org framework will fail to load at runtime even when the path is correct.

## Sources

- [Issues](https://github.com/pvieito/PythonKit/issues)
- [License: Apache-2.0](https://github.com/pvieito/PythonKit/blob/main/LICENSE)
- [pvieito/PythonKit on GitHub](https://github.com/pvieito/PythonKit)
- [README](https://github.com/pvieito/PythonKit/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pvieito-pythonkit
