Comparison to PyCall

The existing package PyCall is another similar interface to Python. Here is a comparison of the designs:

  • Flexibility of conversion. The mechanisms for data conversion from Python to Julia are different. In PyCall, conversion to T (via convert(T,::PyObject)) essentially only takes T into account, so for example when T=Real then the input will always be converted to a Python float, which is then converted to a Cdouble. In PythonCall, conversion takes into account both the target type T and the Python type of the Python object, and an extensible system allows one to declare conversions for any combination. Many conversions for overlapping combinations can be defined and the most specific one takes precedence. Hence in PythonCall, converting to a Real might return an Int (e.g. the input is a int), or Cdouble (e.g. the input is float), or Rational{BigInt}, or...
  • Lossiness of conversion from Python. In PyCall, the default PyAny conversion from Python to Julia can be lossy in the sense that it is impossible to recover the original value exactly. For example a list of ints is converted to a Vector{Int} which is a copy of the data, and therefore modifying the original list is not possible. It is also a source of off-by-one errors, since Vector and list have different indexing semantics. In PythonCall, the default conversion is to PyObject (non-lossy), and even if you convert to Any then by default this will be non-lossy: for example a list will be converted to a PyList which is a Vector-like view of the list.
  • Lossiness of conversion to Python. Similarly, in PyCall the default conversion from Julia to Python can be lossy: a Vector{Int} will be converted to a list of ints for example, losing mutability of the original vector. In PythonCall, only immutable values are truly converted to Python, everything else is wrapped into a Python wrapper around the Julia value: a Vector{Int} is wrapped into a juliacall.VectorValue which is a list-like sequence type
  • Automatic conversion. In PyCall, most function calls, attribute accesses, indexing, etc. of Python objects by default automatically convert their result to a Julia type. In PythonCall the default is to always return PyObject. The latter behaviour provides type-stability. It also makes interacting with Python values more predictable and allows generic programming (where the type of the result is not known). It also allows the user to pick another type to convert to after the fact, whereas since PyCall conversion can be lossy, this is sometimes not possible there.
  • Building.PyCall locates libpython in its build step, so that it is a const in the module code. This makes ccalls and the like straightforward. PythonCall does this at run-time, which slightly complicates the code (although it is abstracted away) but means that the module does not need to be rebulit for different Python versions.
  • Default Python. By default PyCall uses the version of Python in conda and will silently install miniconda for you if it doesn't exist. PythonCall by default simply uses the version of Python in the PATH. Both are customizable through environment variables.
  • Python modules.PyCall has a companion Python module julia for calling Julia from Python. So does PythonCall, but called juliacall. Both of them use PyCall/PythonCall under the hood on the Julia side. The PyCall one is itself about as complex in implementation as PyCall. The PythonCall one is about 50 lines of code (essentially just finding and loading libjulia and the PythonCall module) and provides a single simple entrypoint: the julia Main module.
  • Compatability.PyCall supports Julia 0.7+ and Python 2.7+, whereas PythonCall supports Julia 1.0+ and Python 3.5+. PyCall requires numpy to be installed, PythonCall doesn't (it provides the same fast array access through the buffer protocol and array interface).
  • Startup time.PythonCall takes longer to start than PyCall, largely because there are a lot of wrapper types (juliacall.AnyValue etc.) to compile.