💎 🐍 Crystal meets Python!
Crython is a tool that lets you use Python libraries in Crystal, a programming language. It provides seamless integration between Crystal and Python, allowing you to leverage Python's ecosystem while enjoying Crystal language.
- You need CPython 3.12 or later. CPython 3.14 is the primary test target.
- Make sure
python3-config --cflags --embed --ldflagsworks.
Add this to your dependencies:
dependencies:
crython:
github: kojix2/crythonUse this when integrating Crython into your own Crystal app
- Verify your Python toolchain:
python3-config --cflags --embed --ldflags- Create your minimal app:
require "crython"
Crython.init
Crython.exec("x = 40 + 2")
puts Crython.eval("x").to_cr # 42- Install dependencies and build:
shards install
PYTHON_CFLAGS="$(python3-config --cflags | sed -E 's/(^|[[:space:]])-arch[[:space:]]+(arm64|x86_64)/\1/g')"
crystal build src/main.cr -o app \
--link-flags "$PYTHON_CFLAGS $(python3-config --embed --ldflags) -lm"- Run your app (set runtime library path if needed):
export LD_LIBRARY_PATH=$(python3 -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))"):$LD_LIBRARY_PATH
./appCrython can be run either directly with Crystal commands or via make.
The make commands below are for this Crython repository itself (development, examples, tests), not for your downstream app.
make test/make run ...are convenience shortcuts.- They automatically apply Python linker flags and runtime library paths.
- Run
make doctorto print detected Python and linker settings when troubleshooting build/runtime issues.
When running binaries directly (without make), set LD_LIBRARY_PATH manually:
export LD_LIBRARY_PATH=$(python3 -c \
"import sysconfig; print(sysconfig.get_config_var('LIBDIR'))"):$LD_LIBRARY_PATHThis command adds the Python library directory to LD_LIBRARY_PATH.
CRYTHON_DEBUG=1: Enables Crython debug logs.LD_LIBRARY_PATH=...: Python shared library search path (mainly needed for direct execution).LDFLAGS=...: Override link flags when your Python environment is non-standard.
To use Crython in your Crystal project, add this line:
require "crython"For complex numbers, also add:
require "complex"- Crython initializes one embedded Python runtime per process and reuses it until process exit.
- Call
Crython.initonce before using the library. Passpython_executable: "/path/to/python"only when you need to select a specific environment. - Python globals, imported modules, and definitions persist across calls. Crython does not support interpreter finalization or reinitialization.
session was deliberately removed. CPython exposes finalization APIs, but an embedding library cannot safely promise that Py_Finalize* followed by reinitialization will reset every extension module, Python-owned resource, or live PyObject wrapper. A per-block session would therefore either falsely imply that it resets Python or leave state alive across its boundary. Crython instead exposes the honest lifecycle: initialize once, reuse one interpreter for the process, and treat Python globals as process-lifetime state.
Import a Python module
np = Crython.import("numpy")Use the module
array = np.array([1, 2, 3])
result = array * 2
puts result # [2 4 6]Crython.init
# Execute Python statements
Crython.exec("print('Hello from Python!')")
# Multiple lines of Python statements
Crython.exec(<<-PYTHON)
print('Hello from Python!')
PYTHON
# Evaluate a Python expression and get a PyObject back
value = Crython.eval("1 + 2")
puts value.to_cr # 3
# Import modules and use them
np = Crython.import("numpy")
array = np.array([1, 2, 3])
puts arrayCrython does not expose session finalization or reset.
Convert Crystal objects to Python objects using the to_py method:
42.to_py # Python int
3.14.to_py # Python float
"hello".to_py # Python str
[1, 2, 3].to_py # Python list
{"a" => 1, "b" => 2}.to_py # Python dict
true.to_py # Python bool
nil.to_py # Python None
Complex.new(1, 2).to_py # Python complexConvert Python objects to Crystal objects using the to_cr method:
py_int = 42.to_py
py_int.to_cr # Int64: 42py_float = 3.14.to_py
py_float.to_cr # Float64: 3.14py_str = "hello".to_py
py_str.to_cr # String: "hello"py_list = [1, 2, 3].to_py
py_list.to_cr # Array(PyObject)py_dict = {"a" => 1, "b" => 2}.to_py
py_dict.to_cr # Hash(PyObject, PyObject)py_bool = true.to_py
py_bool.to_cr # Bool: truepy_none = nil.to_py
py_none.to_cr # Nil: nilpy_complex = Complex.new(1, 2).to_py
py_complex.to_cr # Complex: 1+2iYou can also convert Python objects to specific Crystal types:
py_list = [1, 2, 3].to_py
Array(Int32).new(py_list) # Array(Int32): [1, 2, 3]py_dict = {"a" => 1, "b" => 2}.to_py
Hash(String, Int32).new(py_dict) # Hash(String, Int32): {"a" => 1, "b" => 2}Call methods on Python objects
py_str = "hello".to_py
py_str.upper.to_cr # "HELLO"Access attributes
np = Crython.import("numpy")
version = np.attr("__version__").to_cr
puts "NumPy version: #{version}"Call methods with arguments
math = Crython.import("math")
result = math.pow(2, 3).to_cr
puts "2^3 = #{result}" # 8.0Call methods with keyword arguments
plt = Crython.import("matplotlib.pyplot")
plt.plot([1, 2, 3], [4, 5, 6], color: "red", marker: "o")- Use
obj.call("Abc")to call a Python attribute whose name is not a valid Crystal method name. - Use
obj.call("Abc", arg1, arg2)to call it with positional arguments. - Use
obj.invoke(...)to call a callable returned byattr. - Prefer
call()for uppercase Python attribute names such as class constructors:collections.call("Counter", data). - Keep
obj.method_name(...)for simple lowercase methods likemath.sqrt(16.0). - Use
"-".to_py.attr("join")to get a function attribute. - Use
Crython.slice_fullinstead of:. - Use
import?,attr?,call?when a requested module or attribute may be absent. They returnnilonly for that absence; errors raised inside imports, properties, or callables still propagate.
math = Crython.import?("math")
pi = math.try &.attr?("pi")
pow = math.try &.call?("pow", 2, 3)
collections = Crython.import("collections")
counter = collections.call("Counter", [1, 2, 1, 3].to_py)eval and exec have different roles:
Crython.eval("..."): evaluates a Python expression and returns aPyObject.Crython.exec("..."): executes Python statements and returnsNil.
If you pass statements to eval, Crython raises an error with guidance to use exec.
Python-originated failures expose CrythonError#python_error, including the Python exception type, message, and formatted traceback.
The snippets below assume Crython has already been initialized.
begin
Crython.eval("1 / 0")
rescue error : Crython::CrythonError
if details = error.python_error
puts details.type_name # ZeroDivisionError
puts details.traceback
end
endbegin
# Expression evaluation error
Crython.eval("1/0")
rescue ex
puts "Python error: #{ex.message}"
end# Statement execution
Crython.exec("x = 40 + 2")
# Expression evaluation (returns PyObject)
answer = Crython.eval("x")
puts answer.to_cr # 42begin
# This is a statement, so eval raises and suggests exec
Crython.eval("x = 10")
rescue ex
puts ex.message
# => ... Use Crython.exec for statements
endYou can run tests either directly or via make.
Direct execution (explicit link flags):
PYTHON_CFLAGS="$(python3-config --cflags | sed -E 's/(^|[[:space:]])-arch[[:space:]]+(arm64|x86_64)/\1/g')"
crystal spec --link-flags "$PYTHON_CFLAGS $(python3-config --embed --ldflags) -lm"Or use make as a shortcut:
make testOr with uv:
uv run make testEnable Crython debug logs on demand:
make test CRYTHON_DEBUG=1uv run make test CRYTHON_DEBUG=1Direct crystal spec works as long as you pass proper Python link flags.
For more examples, check the examples folder. To build all examples, use:
make examples
Then run:
./bin/hello
Crython.init
np = Crython.import("numpy")
x1 = np.array([1, 2, 3])
x2 = np.array([4, 5, 6])
y = x1 + x2
puts "#{x1} + #{x2} = #{y}" # [1 2 3] + [4 5 6] = [5 7 9]Crython.init
plt = Crython.import("matplotlib.pyplot")
# Create data
x = [1, 2, 3, 4, 5]
y = [1, 4, 9, 16, 25]
# Create plot
plt.plot(x, y, marker: "o", linestyle: "--")
plt.title("Square Numbers")
plt.xlabel("Number")
plt.ylabel("Square")
# Show plot
plt.show- Symbol conversion: Crystal cannot create Symbols at runtime, so Python strings cannot be converted to Crystal Symbols.
- Union types: Converting Python collections with mixed types to Crystal collections with union types is not fully supported.
python3-config usually provides the correct flags for linking. If it doesn't, you can manually set LDFLAGS to your environment's library path. For example, if using micromamba:
LDFLAGS="-L/Users/<your-username>/micromamba/envs/crython/lib -lpython3.14" make examplesReplace <your-username> with your actual username and adjust the path as necessary.
If you get an error like error while loading shared libraries: libpython3.x.so.1.0: cannot open shared object file: No such file or directory, make sure you've set the LD_LIBRARY_PATH correctly:
export LD_LIBRARY_PATH=$(python3 -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))"):$LD_LIBRARY_PATHIf you encounter linking errors during compilation, check that python3-config --cflags --embed --ldflags returns the correct flags for your Python installation.
Fork ➔ Edit ➔ Commit ➔ Pull Request
Romain Franceschini - The original creator of the Crython project