8.2.165-stable Switch to dev

python

@library("python", "0.0.0");

Embeds a real, free-threaded CPython 3.14 interpreter directly into the python.gclib shared module — no RPC, no subprocess, no serialization. GCL and Python code call each other directly, passing live GreyCat objects (fields, Array/Map/Table/Tensor, methods) across the boundary as thin boxed handles.

Quick start

Declare a native function with no body in GCL, and implement it in project.py at the project root:

// project.gcl
native fn hi_python(v: String): String;

fn main() {
    pprint(hi_python("gc_py_lib"));
}
# project.py
def project_hi_python(v: str) -> str:
    return f"Hello, {v}!"
$ ./bin/greycat run
Hello, gc_py_lib!

The Python interpreter is fully embedded (no system Python, no venv). Python::get_version() returns its version string:

fn main() {
    pprint("py_runtime: ${Python::get_version()}");
}

Installing pip dependencies

Add a requirements.txt next to project.gcl and greycat install installs it automatically — no system Python, no venv, no subprocess: the already-embedded interpreter runs pip’s own install command in-process, targeting the same lib/python/lib/python3.14t/site-packages directory the interpreter already has on sys.path (network access to PyPI is still needed, exactly like running pip install yourself would). See example/ for the full, runnable files this section walks through. In short:

# requirements.txt
toml==0.10.2
# project.py
import toml


def project_pip_dep_demo() -> str:
    return toml.dumps({"greycat": {"loves": "python"}})
// project.gcl
native fn pip_dep_demo(): String;

fn run() {
    pprint(pip_dep_demo());
}
$ ./bin/greycat install
Collecting toml==0.10.2 (from -r requirements.txt (line 1))
  Downloading toml-0.10.2-py2.py3-none-any.whl.metadata (7.1 kB)
Downloading toml-0.10.2-py2.py3-none-any.whl (16 kB)
Installing collected packages: toml
Successfully installed toml-0.10.2

$ ./bin/greycat run
[greycat]
loves = "python"

A failed install (a typo’d package name, no matching version, network down) fails greycat install itself with pip’s own error message and a non-zero exit code — nothing silently succeeds.

project.py is not loaded or executed while greycat install (or greycat codegen) is resolving dependencies — only the interpreter itself is brought up, just enough to run pip. That’s what lets a top-level import toml in a never-before-installed checkout work on the very first greycat install, instead of deadlocking: nothing tries to import it before pip has had a chance to install it.

Naming convention: GCL → Python

Any native fn (module-level free function, type method, or static type method) that has no C implementation is resolved to a project.py function by name, eagerly, when the library starts — if no match is found, the library refuses to start rather than failing silently on first call:

GCL declaration project.py function
native fn foo(...) at module mymod def mymod_foo(...)
native fn bar(...) inside type Point in module mymod def mymod_Point_bar(...)
native static fn baz(...) inside type Point in module mymod def mymod_Point_baz(...)

Parameters and the return value are boxed/unboxed automatically (see “Type mapping” below). A Python exception raised inside the call becomes a GreyCat runtime error, printed and propagated to the caller.

Calling GCL from Python

The reverse direction — Python code calling back into GreyCat — goes through the greycat package, importable from any project.py function (only valid while a native call is in progress):

import greycat

def mymod_make_point(x, y):
    p = greycat.new("mymod::Point")   # "module::Type" -> a new, zero-valued object
    p.x = x                            # live field write
    p.y = y
    return p                           # handed back to GCL, ARC-safe

def mymod_use_point(p):
    total = p.x + p.y                  # live field read
    label = p.describe()               # call a GCL instance method (GCL- or native-bodied, either works)
    origin = greycat.call_static("mymod::Point", "origin")  # a static method, no instance needed
    return label
  • greycat.new("module::Type") creates a new object of that type.
  • obj.field / obj.field = value read/write a field dynamically (__getattr__/__setattr__ resolve the field by name against the object’s runtime type — there is no static schema on the Python side).
  • obj.method(*args) resolves method against the object’s runtime type (walking supertypes for a method the object’s type only inherits, per GCL’s no-override rule) and calls it — instance or static alike; a static method reached through an instance simply ignores the receiver.
  • greycat.call_static("module::Type", "name", *args) calls a static method without needing an instance first.

Both obj.method(...) and greycat.call_static(...) only resolve functions declared inside a type { ... } block — instance or static methods. A plain module-level fn (one not declared inside any type) currently cannot be called from Python at all; there is no greycat.call_module_function() or equivalent. If Python code needs to reach one, wrap it in a static method on a type and call that instead.

A GCL-side error raised while one of these calls is in flight (a throw, a rejected field assignment, …) surfaces on the Python side as a RuntimeError — sometimes carrying GreyCat’s own error message, otherwise just "greycat function call failed". The mirror image of a Python exception becoming a GreyCat runtime error on the way in (see “Naming convention” above).

All of the above only work inside a native call (there is no machine to resolve against otherwise) and the objects they hand back are call-scoped: a GcObject/GcArray/GcMap/GcTable/GcTensor handle is only valid until the native call returns — don’t stash one in a module-level variable and reuse it on the next call.

Type mapping

GCL type Python side
int / float / bool int / float / bool
char str (single character)
String str
time / duration / geo greycat.core.time / duration / geo
Array<T> greycat.GcArraylen(a), a[i], a[i] = v, a.append(v)
Map<K, V> greycat.GcMaplen(m), m[k], m[k] = v, del m[k], k in m
Table greycat.GcTablet[row, col], t[row, col] = v, t.rows(), t.cols(), t.get_cell(r, c), t.set_cell(r, c, v)
Tensor greycat.GcTensor — exposes the PEP 3118 buffer protocol: memoryview(t) works with the stdlib alone, numpy.asarray(t) / torch.frombuffer(...) are zero-copy if the caller happens to have them installed
node, nodeTime, nodeIndex, nodeList, nodeGeo greycat.GcNode / GcNodeTime / GcNodeIndex / GcNodeList / GcNodeGeo.ref() returns the raw reference; element resolve/get/set is not implemented yet
any user-defined type greycat.GcObject — dynamic field access + method calls, see above

GcTable also has .to_numpy() / .to_pandas(), which lazily import numpy / import pandas only when called and raise a clear ImportError if the package isn’t installed — the embedding itself imports nothing beyond the Python standard library.

Example: a typed object round trip

// project.gcl
type Point {
    x: int;
    y: int;
    label: String;

    fn describe(): String {
        return "(${this.x}, ${this.y}) ${this.label}";
    }

    static fn origin(): Point {
        return Point { x: 0, y: 0, label: "origin" };
    }
}

native fn make_point(x: int, y: int, label: String): Point;
native fn describe_point(p: Point): String;
# project.py
import greycat

def project_make_point(x, y, label):
    p = greycat.new("project::Point")
    p.x = x
    p.y = y
    p.label = label
    return p

def project_describe_point(p) -> str:
    return p.describe()  # calling back into GCL

IDE type stubs

greycat codegen writes project_types.pyi next to project.py: one class <Name>(GcObject) per non-hidden GCL type in the current program, with typed fields and method signatures (bodies are ... — the file is never imported at runtime, since GcObject field/method access is fully dynamic). It exists purely so an IDE (mypy/pyright/PyCharm) can offer autocomplete and catch typos in project.py.

Bare greycat codegen auto-detects every applicable generator for the project (C bindings from CMakeLists.txt, TypeScript from package.json, and so on — Python stubs are included whenever @library("python") is linked); greycat codegen python runs just this one generator explicitly:

$ ./bin/greycat codegen
# project_types.pyi (excerpt)
class Point(GcObject):
    x: int
    y: int
    label: str
    def describe(self) -> str: ...
    @staticmethod
    def origin() -> "Point": ...

Re-run it after changing any type’s fields or methods.

Threading

Each native call runs directly on whichever GreyCat worker thread dispatched it, under a genuinely GIL-less interpreter (Py_MOD_GIL_NOT_USED, free-threaded Python 3.14) — there’s no global interpreter lock serializing concurrent GreyCat workers against each other. This is controlled by GREYCAT_PYTHON_THREADING (direct is the only implemented mode today; queued — a single dedicated Python thread — is reserved for a future design and refuses to start if requested).

Deployment

A project using this library needs four things to run standalone on another machine, none of which require Python, pip, or network access on the target itself: bin/ (the greycat executable), lib/ (every installed library — including lib/python/, which holds the embedded interpreter, its standard library, and whatever requirements.txt pulled in via greycat install, all under lib/python/lib/python3.14t/site-packages), webroot/ (static assets, if any), and project.gcl/project.py themselves.

Run greycat install once, wherever pip dependencies need to be resolved (locally, or in CI) — it also handles the initial library download and greycat build. Then ship the resulting bin/, lib/, webroot/, project.gcl, and project.py together as-is: the target machine only ever runs bin/greycat run, never greycat install itself.