In this page
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 = valueread/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)resolvesmethodagainst 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.GcArray — len(a), a[i], a[i] = v, a.append(v) |
Map<K, V> |
greycat.GcMap — len(m), m[k], m[k] = v, del m[k], k in m |
Table |
greycat.GcTable — t[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.