Generated Python bindings

PythonTarget emits a Python package with two generated layers and an author-controlled public layer:

mylib_py/
├── __init__.py
├── _lowlevel.py
├── _generated.py
└── _facade.py

Applications normally import from mylib_py. Direct ABI access remains available as mylib_py._lowlevel.

Regenerated low-level bindings

_lowlevel.py is rewritten on every build. It contains:

  • ctypes.Structure definitions for ABI structs and carriers;
  • ctypes signatures for exported entrypoints;
  • generated enums from API metadata;
  • carrier conversion and release helpers;
  • JLWError; and
  • shared-library discovery and loading.

Every structure is followed by an import-time layout check against the sizes and offsets recorded by juliac. A mismatch fails at import rather than silently interpreting memory with the wrong layout.

Do not edit this file: regeneration replaces it.

Generated façade

_generated.py is rewritten on every build. It exposes idiomatic functions for recognized signatures and leaves a TODO: hand-wrap re-export when target-specific policy is required. Do not edit this file: regeneration replaces it.

With @api metadata, generated functions preserve the declared public name, positional/keyword split, keyword defaults, enums, and docstring. Carrier arguments and results become the Python values listed in Supported Julia types. A non-zero status raises JLWError, whose .code and .message attributes retain the library error.

Hand-written @ccallable entrypoints have no API sidecar declaration. The generator still recognizes common carrier shapes and status returns, but it cannot recover Julia-level names, keyword arguments, defaults, or docstrings. Unrecognized structs, raw ownership transfers, and ambiguous policies are deliberately left to a human.

Array coercion

By default a generated wrapper hands an array argument to the carrier's from_numpy, which raises ValueError when the dtype or memory order differs from the library's. With PythonTarget(...; coerce_arrays = true) the wrapper first applies np.asfortranarray(x, dtype=...) to an array of two or more dimensions and np.ascontiguousarray(x, dtype=...) to a vector:

def trace(m):
    _m = CMatrix_borrowed_Float64.from_numpy(np.asfortranarray(m, dtype="float64"))
    return _lowlevel.trace(_m)

Input that does not already match is copied without notice, so the option is off by default.

Author-editable façade

_facade.py is created only if it does not exist, and rebuilding never overwrites it. The starter imports everything _generated.py defines and copies its __all__, so a package with no hand-written layer exposes the generated API unchanged. Keep the file under version control.

A _facade.py written by JuliaLibWrapping 0.2 or earlier contains a full copy of the wrappers and does not import _generated. It keeps working, but a rebuild no longer updates its wrappers. To move to the split layout, delete it, rebuild to get the starter, and add the hand-written parts back.

To change the public API:

  • to add a function, define it in _facade.py and append its name to __all__;
  • to override a generated function under its public name, define it after the star import; the generated one stays reachable as _generated.<name>;
  • to hide a generated name, remove it from __all__.

An override that builds on the generated wrapper reaches it through the module:

import numpy as np

from . import _generated
from ._generated import *  # noqa: F401,F403

__all__ = list(_generated.__all__)


def gramcover(A, tol=1e-8):
    """Cover the columns of `A`, dropping near-duplicates."""
    return _generated.gramcover(np.asarray(A, dtype="float64"), tol)

Package exports

__init__.py is regenerated and re-exports whatever _facade.__all__ lists. Functions added to _facade.py therefore become package-level imports, while the mechanical layer stays explicitly accessible as mylib_py._lowlevel.

Error conversion

An @api entrypoint catches exceptions and returns a JLWResult or direct JLWStatus; the façade raises JLWError on failure and returns the converted value on success:

from mylib_py import scale, JLWError

try:
    scale(bad_input)
except JLWError as err:
    print(err.code, err.message)

For a hand-written entrypoint, the same conversion applies when the return is a direct JLWStatus or a struct containing one. On success an unrecognized result struct remains a low-level ctypes value.

Library loading

For a bundled package the loader searches the configured bundle directory first, then falls back to a shared library beside the package. A privatized bundle records that it can safely coexist with other wrapped libraries. A non-privatized package warns when another JuliaLibWrapping package is already loaded in the process.

See Bundling for distribution and Multiple wrapped libraries in one process for the runtime implications.