This library exposes all OCaml modules as Python modules, generating bindings on the fly.
OCaml
>= 4.13
Python
>= 3.7 (and >= 3.10 for pattern-matching support)
The package can be installed via opam
:
opam install ocaml-in-python
installs the latest release,
opam pin add -k path . && opam install ocaml-in-python
executed in a clone of this repository installs the latest development version.
Once installed via opam
, the package should be registered in the Python environment.
There are two options:
either you register the package with pip
using the following command (requires Python >=3.8):
pip install --editable "`opam var ocaml-in-python:lib`"
or you add the following definition to your environment:
export PYTHONPATH="`opam var share`/python/:$PYTHONPATH"
A very simple mean to test that the bindings are working properly is to invoke the OCaml standard library from Python.
import ocaml
print(ocaml.List.map((lambda x : x + 1), [1, 2, 3]))
# => output: [2;3;4]
In the following example, we invoke the ref
function from the OCaml
standard library to create a value of type int ref
(a mutable
reference to an integer), and the following commands show that the
reference can be mutated from Python (a reference is a record with a
mutable field contents
) and from OCaml (here by invoking the OCaml
function incr
).
>>> x = ocaml.ref(1, type=int)
>>> x
{'contents':1}
>>> x.contents = 2
>>> x
{'contents':2}
>>> ocaml.incr(x)
>>> x
{'contents':3}
In the following example, we compile an OCaml module on the fly from Python.
import ocaml
m = ocaml.compile(r'''
let hello x = Printf.printf "Hello, %s!\n%!" x
type 'a tree = Node of { label : 'a; children : 'a tree list }
let rec height (Node { label = _; children }) =
1 + List.fold_left (fun accu tree -> max accu (height tree)) 0 children
let rec of_list nodes =
match nodes with
| [] -> invalid_arg "of_list"
| [last] -> Node { label = last; children = [] }
| hd :: tl -> Node { label = hd; children = [of_list tl] }
''')
m.hello("world")
# => output: Hello, world!
print(m.height(
m.Node(label=1, children=[m.Node(label=2, children=[])])))
# => output: 2
print(m.of_list(["a", "b", "c"]))
# => output: Node {label="a";children=[Node {label="b";children=[Node {label="c";children=[]}]}]}
try:
print(m.of_list([]))
except ocaml.Invalid_argument as e:
print(e)
# => output: Stdlib.Invalid_argument("of_list")
It is worth noticing that there is no need for type annotations: bindings are generated with respect to the interface obtained by type inference.
findlib
In the following example, we call the OCaml library
parmap
from Python.
import ocaml
ocaml.require("parmap")
from ocaml import Parmap
print(Parmap.parmap(
(lambda x : x + 1), Parmap.A([1, 2, 3]), ncores=2))
# => output: [2, 3, 4]
The function ocaml.require
uses
ocamlfind
to load parmap
.
Bindings are generated as soon as ocaml.Parmap
is accessed
(in the example, at line from ocaml import Parmap
).
Parmap.A
is one of the two constructors of the type Parmap.sequence
.
The generation of bindings is driven by the types exposed by the
compiled module interfaces (*.cmi
files): relying on the *.cmi
files allows the bindings to cover most of the OCaml definitions
(there are some limitations though, see below) and to use the inferred
types for modules whose interface is not explicitly specified by a
.mli
file.
The following conversions are defined for built-in types:
int
, nativeint
int32
, int64
are mapped to Python int
;import ocaml
ocaml.print_endline(ocaml.string_of_int(42))
# => output: 42
print(ocaml.int_of_string("5") + 1)
# => output: 6
string
is mapped to Python str
import ocaml
ocaml.print_endline("Hello, World!")
# => output: Hello, World!
print(ocaml.String.make(3, "a") + "b")
# => output: aaab
char
is mapped to Python str
with a single characterimport ocaml
print(ocaml.int_of_char("a"))
# => output: 97
print(ocaml.char_of_int(65))
# => output: A
bool
is mapped to Python bool
(beware of different case convention:
OCaml values false
and true
are mapped to Python values
False
and True
respectively)import ocaml
print(ocaml.Sys.interactive.contents)
# => output: False
print(ocaml.string_of_bool(True))
# => output: true
float
is mapped to Python float
, and functions taking
floats as arguments can take benefit from the Python automatic
coercion from int
to float
import ocaml
print(ocaml.float_of_int(1))
# => output: 1.0
print(ocaml.cos(0))
# => output: 1.0
array
is mapped to a dedicated class ocaml.array
, which
supports indexing, enumeration, pattern-matching (with Python >= 3.10)
and in-place modification. When an OCaml array is converted to a Python
object, the elements are converted on demand.
There is an implicit coercion to array from all Python iterable types
such as Python lists (but in-place modification is lost).import ocaml
arr = ocaml.Array.make(3, 0)
arr[1] = 1
print(ocaml.Array.fold_left((lambda x,y : x + y), 0, arr))
# => output: 1
ocaml.Array.sort(ocaml.compare, arr)
print(list(arr))
# => output: [0, 0, 1]
print(ocaml.Array.map((lambda x: x + 1), range(0, 4)))
# => output: [|1;2;3;4|]
# With Python 3.10:
match arr:
case [0, 0, 1]:
print("Here")
# => output: Here
It is worth noticing that Array.make
is a polymorphic function
parameterized in the type of the elements of the constructed array,
and by default the type parameter for polymorphic function with
ocaml-in-python
is Py.Object.t
, the type of all Python objects.
As such, the cells of the array arr
defined above can contain any
Python objects, not only integers.
arr[0] = "Test"
print(arr)
# => output: [|"Test";0;1|]
We can create an array with a specific types for cells by
expliciting the type parameter of Array.make
, by using the keyword
parameter type
.
arr = ocaml.Array.make(3, 0, type=int)
arr[0] = "Test"
# TypeError: 'str' object cannot be interpreted as an integer
OCaml list
is mapped to a dedicated class ocaml.list
, which
supports indexing, enumeration and pattern-matching (with Python >= 3.10).
When an OCaml list is converted to a Python
object, the elements are converted on demand.
There is an implicit coercion to list from all Python iterable types
such as Python lists.
OCaml bytes
is mapped to a dedicated class ocaml.bytes
, which
behaves as a mutable collection of characters.
OCaml option
is mapped to a dedicated class ocaml.option
, only
for values of the form Some x
where the type of x
allows the
value None
. If the type of x
does not contain a value None
,
the OCaml value Some x
is mapped directly to the conversion of x
.
Conversely, the value Some x
can be constructed
with ocaml.Some(x)
.
The OCaml value None
is mapped to the Python value None
.
print(ocaml.List.find_opt((lambda x : x > 1), [0,1], type=int))
# => output: None
print(ocaml.List.find_opt((lambda x : x > 1), [0,1,2], type=int))
# => output: 2
print(ocaml.List.find_opt((lambda x : x > 1), [0,1,2]))
# => output: Some(2)
In the last call to find_opt
, the default type parameter is Py.Object.t
which contains the value None
.
exn
is mapped to a dedicated class ocaml.exn
, which is a
sub-class of Python Error
class, and exceptions are converted as
other extension constructors: each exception is a sub-class of ocaml.exn
,
and values can be indexed (if the exception constructor takes parameters),
accessed by field name (for inline records) and supports
pattern-matching (with Python >= 3.10).
There is an implicit coercion from other sub-classes of Python Error
class
to Py.E
, the OCaml exception defined in pyml
for Python exceptions.
If an exception is raised between OCaml and Python code, the exception is
converted and raised from one side to the other.try:
ocaml.failwith("Test")
except ocaml.Failure as e:
print(e[0])
# => output: Test
in_channel
and out_channel
are mapped to FileIO
objects.
In the following example, the OCaml function open_out
is used to create
a new file test
, and the string Hello
is written in this file through
the Python method write
. Then, the file test
is opened for reading
with the Python built-in open
, and the channel is read with the OCaml
function really_input_string
.with ocaml.open_out("test") as f:
f.write(b"Hello")
with open("test", "r") as f:
print(ocaml.really_input_string(f, 5))
# => ouput: Hello
floatarray
is mapped to a dedicated class ocamlarray
,
which derives from numpy
array (numpy
is required for the
support of floatarray
).OCaml functions of type 't_1 -> ... -> 't_n -> 'r
are mapped to Python callable objects with n
arguments.
Labelled arguments are mapped to mandatory keyword arguments,
and optional arguments are mapped to optional keyword arguments.
For polymorphic functions, the type parameters are assumed to be
Py.Object.t
, except if there is a keyword argument type
:
the associated value can either be a single type if there is
a single type parameter, or a tuple of types giving the type
parameters in the order of their apparition in the function signature,
or a dictionary whose keys are the names of the type parameters
(e.g., "a"
for 'a
).
OCaml tuples of type 't_1 * ... * 't_n
are mapped to OCaml tuples with n
components.
Each OCaml type definition introduces a new Python class, except for type aliases, that are exposed as other names for the same class.
Records are accessible by field name or index (in the order of the
field declarations), and the values of the fields are converted on
demand. Mutable fields can be set in Python. In particular, the ref
type defined in the OCaml standard library is mapped to the Python
class ocaml.ref
with a mutable field content
.
Records support pattern-matching (with Python >= 3.10).
There is an implicit coercion from Python dictionaries with matching
field names.
For variants, there is a sub-class by constructor, which behaves either as a tuple or as a record. The values of the arguments are converted on demand. Variants support pattern-matching (with Python >= 3.10).
Sub-modules are mapped to classes, which are constructed on demand.
For instance, the module Array.Floatarray
is exposed as
ocaml.Array.Floatarray
, and, in particular, the function
Array.Floatarray.create
is available as
ocaml.Array.Floatarray.create
.
The following traits of the OCaml type system are not supported (yet):