# libpython-clj: calling Python from Clojure without writing a bridge

> A Clojure library that embeds the Python interpreter in a JVM process, so Python modules load as if they were Clojure namespaces and Clojure can extend Python objects. The trade-offs are the GIL, a name change that strands the older artifact, and a dependency on finding libpython at runtime.

**clj-python/libpython-clj** — Python bindings for Clojure

- Repository: https://github.com/clj-python/libpython-clj
- Stars: 1,211 · Forks: 74
- Language: Clojure
- License: EPL-2.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/clj-python-libpython-clj

## The problem: two languages, one process

libpython-clj is Python bindings for Clojure, and the vision section says the aim is to integrate Python into Clojure at a deep level. The concrete meaning of deep is given in two clauses: to be able to load and use Python modules almost as if they were Clojure namespaces, and to be able to use Clojure to extend Python objects.

The feature list is explicit that the bridge works in both directions. Use Python in your Java, and use some Java in your Python. That second direction is the unusual one, and it is what makes the library a genuine bidirectional bridge rather than a one-way wrapper.

The usage section shows how small the surface is. You import a require namespace, import a Python module with a macro, and call it:

```clojure
user> (require '[libpython-clj2.require :refer [require-python]])
...logging info....
nil
user> (require-python '[numpy :as np])
nil
user> (def test-ary (np/array [[1 2][3 4]]))
#'user/test-ary
user> test-ary
[[1 2]
 [3 4]]
```

The library namespace is `libpython-clj2` and the artifact on Clojars is `clj-python/libpython-clj`. The README asks you to avoid deprecated versions such as `[cnuernber/libpython-clj "1.36"]` and notes the name change, which is a hint that there was an earlier artifact under a different group and coordinate.

The licence is EPL-2.0, with a `LICENSE` file at the repository root, and the repository has no GitHub releases, so the version published to Clojars is the version marker.

## The GIL is the central design constraint

Any library that embeds CPython in a JVM has to deal with the Global Interpreter Lock, and libpython-clj's approach is visible in the API.

The example code in the README wraps Python work in a context:

```clojure
(defn face->feature
  [img-path]
  (py/with-gil-stack-rc-context
    (if-let [new-img (cv2/imread img-path))
      ...)))
```

The `with-gil-stack-rc-context` macro acquires the GIL for the body of the block and manages the reference counts of the Python objects that cross the boundary, which is what stops a Python object created inside the block from being collected while Clojure still holds a reference to it.

The Java API documentation carries a warning that deserves repetition: due to performance considerations, not all methods are protected via automatic GIL management. So the library offers automatic handling where it can, and leaves the rest to the caller, and you have to read the namespace documentation to know which is which. That is a real ergonomic cost, and it is the kind of thing that pays off only after you have read that doc rather than after you have debugged a deadlock.

The feature list also notes that Python objects are linked to the JVM garbage collector, so that when they are no longer reachable from the JVM their references are released. Scope-based resource contexts are documented separately in `topics/scopes-and-gc.md` for the cases where you want explicit control.

## Finding libpython at runtime, which is also the fragile part

The library does not bundle an interpreter. It finds the Python installation dynamically, and the feature list gives the reason: this allows one system to run on multiple versions of Python.

The mechanism is described in the environments section, and it is charmingly low-tech. The library launches the python3 executable and prints out various bits of configuration as JSON. It parses that JSON and uses the output to attempt to find the `libpython3.Xm.so` shared library. In other words, discovery works by asking the interpreter where it is, rather than by looking in a hard-coded prefix.

That is the right idea for conda environments, where the interpreter lives in a versioned directory. The configuration namespace makes it explicit:

```clojure
(ns my-py-clj.config
  (:require [libpython-clj2.python :as py]))

;; When you use conda, it should look like this.
(py/initialize! :python-executable "/opt/anaconda3/envs/my_env/bin/python3.7"
                :library-path "/opt/anaconda3/envs/my_env/lib/libpython3.7m.dylib")
```

Both the executable and the library path are given explicitly, and the README says the namespace should run when the REPL is up, wired in through `:repl-options {:init-ns my-py-clj.config}` in `project.clj`.

The README is candid that the support is not perfect and that some understanding of the mechanism is important to diagnose errors and issues. The concrete risk: a deployment image that has a python3 on the PATH but not a matching shared library, or a Windows or macOS layout the discovery does not handle, will fail at initialisation rather than degrade. The mitigation is to set the two paths explicitly, as the conda example does.

## Using the library from Java, with no Clojure required

One of the more surprising capabilities is that the bridge does not require you to write Clojure. The README points to a Java API and says you can use libpython-clj from Java with no Clojure required. The class is included with the jar, so you put the jar on the classpath and the import works:

```java
import libpython_clj2.java_api;
```

The caveat follows immediately: you need to read the namespace doc carefully, because not all methods are protected via automatic GIL management, for performance reasons.

The same paragraph says the Java integration includes support for extremely efficient data copies to numpy objects and callbacks from Python to Java. Both of those are the two hard parts of any interop layer, and the fact that they are handled here is what makes the Java entry point a serious option rather than a curiosity.

This is a genuinely useful shape for organisations that have a Java service and a Python model or numerical library, and no appetite for a second runtime in the deployment. The Clojure dependency is not on the classpath requirement list; the jar is what you need.

The cost of that path is that you inherit the same GIL discipline. Java code calling into Python through this API is making the same calls the Clojure API makes, with the same caveats about which ones manage the lock for you.

## make-fastcallable, numpy copies and the shape of the performance story

Performance is discussed in specific terms rather than in benchmarks, which is the more useful framing for an interop library.

The `make-fastcallable` feature addresses the case of calling a small function repeatedly, and the README says you can now call it about twice as fast. It also names the better optimisation: call the function once with numpy array arguments, so that the per-call overhead is amortised over a bulk operation. The README is honest that not all use cases are amenable to that pathway, and that they did what they could for the rest.

That is a real design statement about where the wins are. Per-call crossing costs dominate when the function is small, and the library attacks that with a dedicated wrapper. When the payload is an array, the win comes from copying the data once rather than element by element, which is what the efficient numpy data copies in the Java API support.

The README also claims 20 to 30 percent better performance, without saying against which baseline, and lists 32-bit support alongside JDK 17 and Mac M-1 support as headline features. For JDK 17 there is a specific requirement: you need to enable the foreign module, and the README links to a working alias in `deps.edn`.

The dependency story is worth noting. The library is built mainly upon cnuernber/dtype-next for the array and container types, and the JNA library for native access. Those two are what you inherit alongside it.

## Static code generation against require-python at load time

The performance work also shows up in the loading path. The `require-python` macro, which you saw in the REPL example, resolves a Python module at runtime, and the README says the static code generation feature exists because generated namespaces are safe to use with AOT and load much faster than the analogous `require-python` calls.

The generator is `write-namespace`, and the difference is worth understanding. With `require-python`, the module is imported at the point the macro is evaluated, which means the import cost lands inside your application's startup, and the README notes that these namespaces will not automatically initialize the python subsystem, so `initialize!` must be called first or a nice exception is thrown.

That ordering requirement is a real operational detail. If you generate a namespace that calls into Python at load time, and the Python subsystem has not been initialised, you get an exception rather than a partially working state. The generated namespace is safe to use with AOT compilation, which matters for GraalVM native image builds and for anything that runs from a compiled uberjar.

The REPL-oriented design is the other half of the story. The feature list describes a REPL oriented design meaning fast, smooth, iterative development, and the vision talk given at Clojure Conj 2019 outlines the intent behind it.

## Embedding Clojure in Python, and where the project stands

The most recent headline feature in the README is that you can now embed Clojure in Python, launching a Clojure REPL from a Python host process. That closes the loop on the bidirectional claim: the same library lets a Python process start a Clojure runtime, not only the other way round.

Alongside it there is a Java API, the fastcallable wrapper, JDK 17 and Mac M-1 support, 32-bit support, the static code generator, and the code generation for a facial recognition embedding which the README offers as a concrete example of what deep integration looks like. That example is worth reading in full, because it is the clearest statement of the intent: it loads mxnet, cv2 and numpy, binds an mxnet module to a layer of a loaded checkpoint, and converts the output through `dtype/make-container` into a Java float32 array.

Carin Meier is credited with posts on plotting with pyplot and on advanced text generation with a Hugging Face model, plus an examples repository.

For the practical picture: the repository is not archived and the last push was on 2026-08-10, there are no GitHub releases but there is a Clojars badge and a `CHANGELOG.md`, the project is published as `clj-python/libpython-clj` while the namespaces use the `libpython-clj2` prefix, and the licence is EPL-2.0. The `topics/` directory in the tree holds the prose documentation on scopes and garbage collection and on usage, and `questions/` and `testcode/` directories suggest the project keeps its open questions in the repository rather than in an issue tracker.

## Conclusion

libpython-clj fits teams that want one process running both Clojure and Python, particularly where the Python side is a machine learning or numerical stack that has no Clojure equivalent, and that value REPL-driven iteration. It does not fit a team that wants a small dependency surface, since it brings in dtype-next and JNA, and it does not fit a deployment that cannot locate a shared libpython at runtime, because the library finds the Python installation dynamically rather than bundling it. Before adopting it, read the namespace documentation on which methods are protected by automatic GIL management and which are not, use the `clj-python/libpython-clj` artifact name rather than the deprecated `cnuernber/libpython-clj` one, and confirm your JDK and platform are covered, since JDK 17 requires enabling the foreign module through a working alias in `deps.edn`.

## FAQ

### How do I load a Python module from Clojure with libpython-clj?

Require the library and call `require-python` with the module and its aliases, for example `(require-python '[numpy :as np])`. The README also recommends setting up a config namespace that calls `py/initialize!` with the python executable and library path, wired in through the REPL init namespace.

### What is the difference between libpython-clj and libpython-clj2?

The project was renamed, and the README asks you to avoid deprecated versions such as `[cnuernber/libpython-clj "1.36"]`, noting the name change. Current usage is the `libpython-clj2` namespaces and the `clj-python/libpython-clj` artifact.

### How does libpython-clj find the Python installation?

It launches the python3 executable and prints out configuration as JSON, then parses that output to locate the `libpython3.Xm.so` shared library, so one system can run multiple Python versions. You can also set the executable and library path explicitly through `py/initialize!`.

### Can I use libpython-clj from Java without writing Clojure?

Yes. The Java API class is included with the jar, so you put the jar on the classpath and `import libpython_clj2.java_api;` works. The namespace documentation warns that not all methods are protected by automatic GIL management.

### How do I speed up repeated calls into Python?

Use `make-fastcallable` for a small function called repeatedly, which the README says is about twice as fast. The better optimisation is to call the function once with numpy array arguments, and the Java API includes support for efficient data copies to numpy objects.

## Sources

- [clj-python/libpython-clj on GitHub](https://github.com/clj-python/libpython-clj)
- [Issues](https://github.com/clj-python/libpython-clj/issues)
- [License: EPL-2.0](https://github.com/clj-python/libpython-clj/blob/master/LICENSE)
- [README](https://github.com/clj-python/libpython-clj/blob/master/README.md)

---

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