Open-source project
JuliaPy/PythonCall.jl avatar
JuliaPy/PythonCall.jl

PythonCall.jl: Calling Python From Julia and Julia From Python

Python and Julia in harmony.

1,080 stars89 forksJuliaMIT

At a glance

What is it?
PythonCall and its Python side package JuliaCall give both languages a symmetric bridge, with an extensible conversion layer, non-copying access to mutable Python objects and per-project Conda isolation for Python dependencies. The current release is 0.9.37 and it supports Julia 1.10 and newer with Python 3.10 and newer.
Who is it for?
PythonCall fits a Julia project that needs a Python library, such as a machine learning stack, without giving up type stability or writing a wrapper layer by hand, and it fits a Python team that wants to call a Julia routine for numerical work. It is a poor fit if you want results to come back as native Julia values automatically, because the default is to leave them as Python objects.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Julia, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A Symmetric Bridge, Not a One-Way Bridge

PythonCall brings Python and Julia together with a symmetric interface. You can call Python code from Julia and Julia code from Python, and the design goal is stated plainly in the summary line: simple syntax, so the Python code looks like Python and the Julia code looks like Julia.

That last point is the one that separates this from a foreign function interface written by hand. The interface is not a Julia wrapper library with Python naming conventions, nor a Python wrapper with Julia conventions. Each side reads as the language it is written in, which matters when you are handing a function to a colleague who only knows one of the two.

The feature list covers the ground. There are intuitive and flexible conversions between Julia and Python, with the promise that anything can be converted and that you are in control. There is fast non-copying conversion of numeric arrays in either direction, so you can modify Python arrays such as bytes, array.array or numpy.ndarray from Julia, and Julia arrays from Python. There are helpful wrappers, so Python sequences, dictionaries, arrays, dataframes and IO streams can be interpreted as their Julia counterparts and the other way round. And there are beautiful stack-traces, which is an underrated feature in an interop layer because the failure mode of interop is a stack trace that shows two languages interleaved with no useful frames.

The project carries an Active status badge and describes itself as having reached a stable, usable state and being actively developed.

Two Package Names, One Repository

The project ships as two packages, and the naming is worth understanding before you install anything.

On the Julia side the package is PythonCall, the Julia module you import. On the Python side the package is juliacall, and the README calls it the Python module JuliaCall. Both come from the same repository, JuliaPy/PythonCall.jl, and the repository tree shows the two sides side by side, with src/ for the Julia implementation and pysrc/ for the Python one.

The Python package metadata confirms the split. The distribution is named juliacall, at version 0.9.37, with the description Julia and Python in seamless harmony, and it requires Python 3.10 or newer and below 4. Its single runtime dependency is juliapkg, which is the package manager bridge.

So a Julia project depends on PythonCall from the Julia package registry, and a Python project depends on juliacall from PyPI, and the two halves find each other through juliapkg, which is what makes the setup work without you having to point one side at the other by hand.

The repository also carries the pieces a serious interop library needs: a docs/ directory with both stable and dev documentation builds, an ext/ directory for the package extensions mechanism, a pytest/ directory, a test/ directory, a benchmark/ directory with a BENCHMARKS.md, and CITATION.bib for anyone citing the work.

The Conversion Rules Are the Real Design Decision

Everything about how the two languages interact follows from one policy, and the README states it as a difference from PyCall: PythonCall by default never copies mutable objects when converting, but instead directly wraps the mutable object. This means that modifying the converted object modifies the original, and conversion is faster.

That single choice explains most of the API. If you ask for a Python list and you get a wrapper around the actual list, then pushing an element onto it changes the Python object, and there is no surprise copy. The same applies to dictionaries, numpy arrays and the rest.

The second policy is about return values, and it is the one most people will notice first. PythonCall does not usually automatically convert results to Julia values, but leaves them as Python objects. The reason given is that this makes it easier to do Pythonic things with these objects, for example accessing methods, and that it is type-stable.

Type stability is the argument to dwell on. A Julia function that returns Any because the return value might be a Julia number or a Python object is slow and hard to reason about, and the compiler cannot specialise it. Leaving the result as a Python object keeps the type known, which means the call is predictable in a hot loop.

The cost is that you write the conversion yourself, deliberately, at the point where you want it, rather than having it happen invisibly. The README phrases it as you being in control, and for a library aimed at people who write Julia, that is the appeal.

Non-Copying Numeric Arrays in Both Directions

The numeric array case is called out separately because it is the one that matters for performance.

The README promises fast non-copying conversion of numeric arrays in either direction, and names the types involved on the Python side: bytes, array.array and numpy.ndarray. So a Python numpy array can be modified from Julia, and a Julia array can be modified from Python, and in both cases the change is visible in the other language, because no copy was made.

This is a different claim from the general one about mutable objects, because numeric arrays are the case where a copy is often silently acceptable and therefore frequently happens. A wrapper that copies a numpy array on access would be correct in every functional sense and would quietly destroy the performance advantage of using Julia for the numerics at all, so the no-copy guarantee here is what makes the interop worth it for a numeric workload.

The examples in the repository show the shape of the workflow from both directions. In the first, a Julia notebook under Pluto loads the Iris dataset as a Julia DataFrame with RDatasets, converts it with pytable(df) into a Python Pandas DataFrame, and uses the Python package Seaborn to produce a pair-plot, which is automatically displayed.

julia
pytable(df)

In the second, an IPython notebook uses the Python side to generate random training data with NumPy, constructs and trains a neural network model with Julia's Flux, and plots sample output with MatPlotLib. The division of labour in that example is the interesting part, since the numerical training is in Julia and the plotting and data generation stay in Python.

Python Dependencies Live in a Separate Conda Environment

The dependency story is the part of a Python-Julia bridge that causes the most friction in practice, and PythonCall handles it with CondaPkg.

PythonCall installs dependencies into a separate Conda environment for each Julia project using CondaPkg. The README states the reason: each Julia project can have an isolated set of Python dependencies. There is a CondaPkg.toml in the repository root, which is the file where those requirements are declared.

The alternative would be to install into the user's global Python environment, and that is the thing that causes the classic broken-environment problem, where a package upgrade for one project breaks another. Isolating per Julia project means the Python side of the stack is as reproducible as the Julia side, and the manifest that pins the Julia environment has a counterpart for the Python packages.

The juliapkg dependency in the Python package metadata is the mechanism that makes this work from the Python side.

For a team, that also means the Python dependency set is a reviewed artefact in the repository rather than something that drifts on individual machines.

How It Differs From PyCall

The README has a section comparing this with the existing package PyCall, and it is worth reading because the differences are design choices rather than bug fixes.

PythonCall supports a wider range of conversions between Julia and Python, and the conversion mechanism is extensible, which means you can define how a type of your own crosses the boundary rather than living with what is built in.

PythonCall by default never copies mutable objects when converting, but instead directly wraps the mutable object, so modifying the converted object modifies the original and conversion is faster.

PythonCall does not usually automatically convert results to Julia values, but leaves them as Python objects, which makes it easier to do Pythonic things with these objects, for example accessing methods, and it is type-stable.

And PythonCall installs dependencies into a separate Conda environment for each Julia project using CondaPkg.

The last point is the one that most changes the day to day experience of a migration. Wrapping in place is closer to how Python objects behave in Python, so code written against PyCall often works with fewer changes than the interface difference suggests.

The counterpoint is the return value policy. A PyCall user accustomed to receiving a Julia array from a Python function has to adjust, and that adjustment is the price of the type stability.

Supported Systems and the Documentation Surface

The compatibility statement is short and specific: the project is tested on Windows, macOS and Linux, 64-bit, with Julia 1.10 upwards and Python 3.10 upwards.

The Julia package has its own Project.toml, and the build is handled by hatchling on the Python side with the wheel target pointing at the pysrc/juliacall package. The Python development dependencies are pytest with pytest-cov, flake8, numpy and nbval.

That is consistent with the repository layout, where examples/ holds the flux.ipynb and seaborn.ipynb notebooks along with their rendered images, and the pytest/ directory sits alongside the Julia test/ directory. Two test suites, one per language.

The documentation is published in two versions, stable and dev, at juliapy.github.io/PythonCall.jl, and the README points newcomers there first. The changelog is at the root, there is a CITATION.bib for academic use, a BENCHMARKS.md for the performance work, an AGENTS.md for the automated contributors, and a bump.jl script for the version bump.

The release history shows a busy 0.9 line, with v0.9.37 on 2026-10-13, v0.9.36 on 2026-09-18 and v0.9.35 on 2026-06-08, and the last push was on 2026-10-13.

Editorial conclusion

PythonCall fits a Julia project that needs a Python library, such as a machine learning stack, without giving up type stability or writing a wrapper layer by hand, and it fits a Python team that wants to call a Julia routine for numerical work. It is a poor fit if you want results to come back as native Julia values automatically, because the default is to leave them as Python objects. Before adopting it, check the juliapkg and CondaPkg setup, since Python dependencies are installed into a separate Conda environment per Julia project, and pin the version, since the package is on a 0.9 line with frequent patch releases. It is MIT licensed, the newest release is 0.9.37 from 2026-10-13, and the last push was on 2026-10-13.

Frequently asked questions

What is PythonCall.jl and what does it provide?

It is a package for calling Python code from Julia and Julia code from Python through a symmetric interface, with simple syntax on each side, extensible conversions, fast non-copying conversion of numeric arrays, helpful wrappers for sequences, dictionaries, arrays, dataframes and IO streams, and stack traces that read well.

How do I install PythonCall and JuliaCall?

The Julia side is the PythonCall package and the Python side is the juliacall package, both from this repository. The Python package requires Python 3.10 or newer and below 4, and depends on juliapkg, while the project supports Julia 1.10 upwards on Windows, macOS and Linux.

How does PythonCall handle conversion of mutable objects?

By default it never copies them, but directly wraps the mutable object, so modifying the converted object modifies the original and the conversion is faster. Numeric arrays such as bytes, array.array and numpy.ndarray convert without copying in either direction.

Why are my Python dependencies installed in a Conda environment?

Because PythonCall installs dependencies into a separate Conda environment for each Julia project using CondaPkg, so each project can have an isolated set of Python dependencies. The requirements are declared in the CondaPkg.toml file in the repository root.

What are the main differences between PythonCall and PyCall?

PythonCall supports a wider range of conversions and an extensible conversion mechanism, it never copies mutable objects by default but wraps them, and it does not usually automatically convert results to Julia values, leaving them as Python objects, which the README notes makes Pythonic usage easier and is type-stable.

Official sources

  1. JuliaPy/PythonCall.jl on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/juliapy-pythoncall-jl.svg)](https://hysenlabs.com/projects/juliapy-pythoncall-jl)

Was this useful?