Run this notebook

Use Livebook to open this notebook and explore new ideas.

It is easy to get started, on your machine or the cloud.

Click below to open and run it in your Livebook at .

(or change your Livebook location)

# Python Integration in Lux ```elixir Mix.install([ {:lux, ">= 0.5.0"}, {:kino, "~> 0.14.2"} ]) Application.ensure_all_started([:lux]) ``` ## Overview <a href="https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2FSpectral-Finance%2Flux%2Fblob%2Fmain%2Flux%2Fguides%2Flanguage_support%2Fpython.livemd" style="display: none"> <img src="https://livebook.dev/badge/v1/blue.svg" alt="Run in Livebook" /> </a> Lux provides first-class support for Python, allowing you to leverage Python's rich ecosystem of libraries and tools in your agents. This guide explains how to use Python effectively with Lux. ## Writing Python Code ### Using the ~PY Sigil The `~PY` sigil allows you to write Python code directly in your Elixir files: ```elixir defmodule MyApp.Prisms.DataAnalysisPrism do use Lux.Prism, name: "Data Analysis" require Lux.Python import Lux.Python def handler(input, _ctx) do result = python variables: %{data: input} do ~PY""" import numpy as np # Process input data array = np.array(data) mean = np.mean(array) std = np.std(array) { "mean": float(mean), "std": float(std), "shape": array.shape } """ end {:ok, result} end end ``` Let's try it out with some real data: ```elixir require Lux.Python import Lux.Python data = [1, 2, 3, 4, 5] python variables: %{data: data} do ~PY""" import numpy as np array = np.array(data) mean = np.mean(array) std = np.std(array) { "mean": float(mean), "std": float(std), "shape": array.shape } """ end ``` Key features: - Multi-line Python code with proper indentation - Variable binding between Elixir and Python - Automatic type conversion - Error handling and timeouts ### Custom Python Modules You can add your own Python modules under the `priv/python` directory: ``` priv/python/ ├── my_module/ │ ├── __init__.py │ ├── analysis.py │ └── utils.py ├── another_module.py └── pyproject.toml ``` These modules can be imported and used in your Lux code: ```elixir python do ~PY""" from my_module.analysis import process_data from my_module.utils import format_output result = process_data(input_data) formatted = format_output(result) """ end ``` ## Package Management ### Using Poetry Lux uses Poetry for Python package management. The `pyproject.toml` file in `priv/python` defines your dependencies: ```toml [tool.poetry] name = "lux-python" version = "0.1.0" description = "Python support for Lux framework" [tool.poetry.dependencies] python = "^3.9" numpy = "^1.24.0" pandas = "^2.0.0" scikit-learn = "^1.2.0" [tool.poetry.dev-dependencies] pytest = "^7.0.0" black = "^23.0.0" ``` To install dependencies: ```bash cd priv/python poetry install ``` ### Importing Packages Use `Lux.Python.import_package/1` to dynamically import Python packages: ```elixir # Import numpy for numerical operations {:ok, %{"success" => true}} = Lux.Python.import_package("numpy") # Let's use it in a calculation python do ~PY""" import numpy as np # Create an array and perform operations array = np.array([1, 2, 3, 4, 5]) mean = np.mean(array) std = np.std(array) {"mean": float(mean), "std": float(std)} """ end ``` ## Type Conversion Lux automatically handles type conversion between Elixir and Python: | Elixir Type | Python Type | |-------------|-------------| | `nil` | `None` | | `true`/`false` | `True`/`False` | | Integer | `int` | | Float | `float` | | String | `str` | | List | `list` | | Map | `dict` | | Struct | `dict` | Let's see type conversion in action: ```elixir python variables: %{ number: 42, text: "hello", list: [1, 2, 3], map: %{key: "value"} } do ~PY""" # Check types of converted variables result = { "number_type": str(type(number)), "text_type": str(type(text)), "list_type": str(type(list)), "map_type": str(type(map)) } # Show some conversions result["conversions"] = { "none_to_nil": None, "bool_to_atom": True, "int_to_integer": 42, "list_to_list": [1, "two", 3.0] } result """ end ``` ## Error Handling Python errors are converted to Elixir exceptions. You have several options for handling them: ### 1. Handle Errors in Python ```elixir # Handle errors directly in Python code python do ~PY""" try: # This will raise a NameError result = undefined_variable except NameError as e: result = {"error": str(e)} except Exception as e: result = {"error": f"Unexpected error: {str(e)}"} result """ end ``` ### 2. Handle Exceptions in Elixir ```elixir # Use try/rescue in Elixir try do python! do ~PY""" # This will raise a NameError undefined_variable """ end rescue RuntimeError -> "Caught Python error" end ``` ### 3. Pattern Match on Results ```elixir # Use pattern matching with python/2 case python do ~PY""" import math try: result = math.sqrt(-1) # This will raise a ValueError {"success": True, "result": result} except ValueError as e: {"success": False, "error": str(e)} """ end do {:ok, %{"success" => true, "result" => result}} -> "Got result: #{result}" {:ok, %{"success" => false, "error" => error}} -> "Got error: #{error}" {:error, error} -> "Python execution failed: #{error}" end ``` ## Testing ### Elixir Tests Test your Python code using the standard Elixir testing tools: ```elixir defmodule MyApp.Prisms.DataAnalysisPrismTest do use UnitCase, async: true import Lux.Python test "processes data correctly" do result = python variables: %{data: [1, 2, 3, 4, 5]} do ~PY""" import numpy as np np.mean(data) """ end assert {:ok, 3.0} = result end test "handles errors gracefully" do result = python do ~PY""" try: undefined_variable except NameError: {"status": "error", "message": "Variable not defined"} """ end assert {:ok, %{"status" => "error"}} = result end end ``` ### Python Tests You can write native Python tests under `priv/python/tests/`. These tests use pytest and can be run directly from your project root using the `mix python.test` command. Here's an example test structure: ``` priv/python/ ├── tests/ │ ├── __init__.py │ ├── test_eval.py # Tests for eval module │ ├── test_analysis.py # Tests for your custom modules │ └── test_utils.py # Tests for utility functions ├── my_module/ │ ├── __init__.py │ ├── analysis.py │ └── utils.py └── pyproject.toml ``` Example test file (`test_analysis.py`): ```python """Tests for the analysis module.""" import pytest from my_module.analysis import process_data def test_process_data(): """Test basic data processing functionality.""" input_data = [1, 2, 3, 4, 5] result = process_data(input_data) assert "mean" in result assert "std" in result assert result["mean"] == 3.0 def test_process_data_empty(): """Test handling of empty input.""" with pytest.raises(ValueError, match="Input data cannot be empty"): process_data([]) def test_process_data_types(): """Test type conversion and validation.""" result = process_data([1, 2.5, "3"]) # Mixed types assert isinstance(result["mean"], float) assert isinstance(result["std"], float) ``` To run the Python tests: ```bash # Run all Python tests mix python.test # Run specific test file mix python.test tests/test_analysis.py # Run tests with specific marker mix python.test --marker=integration # Run tests with pytest options mix python.test --verbose --capture=no ``` The test runner will: 1. Ensure the test directory exists and create it if needed 2. Install pytest and pytest-cov if not already installed 3. Run the tests with coverage reporting 4. Display test results and coverage information ### Test Best Practices 1. **Test Organization** - Keep Python tests in `priv/python/tests/` - Use descriptive test names with docstrings - Group related tests in classes or modules - Follow pytest best practices for fixtures and markers 2. **Test Coverage** - Test both success and error paths - Test type conversions thoroughly - Test edge cases and boundary conditions - Use pytest's parametrize for multiple test cases 3. **Test Performance** - Use pytest fixtures for setup/teardown - Mock external services and heavy operations - Use appropriate scopes for fixtures - Consider test isolation needs 4. **Test Maintenance** - Keep tests focused and readable - Document test requirements in docstrings - Update tests when behavior changes - Use CI/CD to run tests automatically ## Best Practices 1. **Module Organization** - Keep related Python code in modules under `priv/python` - Use clear module and function names - Follow Python style guidelines (PEP 8) 2. **Performance** - Batch operations to minimize cross-language calls - Use NumPy for numerical operations - Consider memory usage with large datasets 3. **Error Handling** - Handle expected errors in Python with try/except - Use pattern matching in Elixir for high-level flow control - Provide meaningful error messages - Clean up resources in error cases 4. **Testing** - Test both success and error cases - Verify type conversions - Test with realistic data ## Coming Soon Lux will soon support defining components entirely in Python: ```python from lux import Prism, Beam, Agent class MyPrism(Prism): name = "Python Prism" description = "A prism implemented in Python" def handler(self, input, context): try: # Process input result = self.process_data(input) return {"success": True, "data": result} except Exception as e: return {"success": False, "error": str(e)} ``` This will allow you to: - Write agents entirely in Python - Define prisms and beams in Python - Use Python's class system - Leverage Python's async capabilities Stay tuned for updates!
See source

Have you already installed Livebook?

If you already installed Livebook, you can configure the default Livebook location where you want to open notebooks.
Livebook up Checking status We can't reach this Livebook (but we saved your preference anyway)
Run notebook

Not yet? Install Livebook in just a minute

Livebook is open source, free, and ready to run anywhere.

Run on your machine

with Livebook Desktop

Run in the cloud

on select platforms

To run on Linux, Docker, embedded devices, or Elixir’s Mix, check our README.

PLATINUM SPONSORS
SPONSORS
Code navigation with go to definition of modules and functions Read More ×