Use the built-in global object `python` when Expr needs to run Python code.
Python integration is useful for calculations, text processing, file processing, external libraries, and longer worker-style tasks that communicate with Expr through events and commands.
Use synchronous Python calls when the script can wait. Use `python_task` when Python should run asynchronously.
Use `python.eval(expression)` for a simple Python expression.
value = python.eval('2 + 3 * 5'); print(value); // 17
This is best for small expressions. For multi-line Python code, use `run_code()` or `python.code(…)`.
Use `python.run_code(code)` when Expr should wait until Python finishes.
The Python code can assign `result` to return a value to Expr.
value = python.run_code('result = 40 + 2'); print(value); // 42
For multi-line code, triple double-quoted Expr strings are convenient.
code = """ total = 0 for n in range(1, 6): total += n result = total """; print(python.run_code(code)); // 15
Synchronous calls block the Expr script until Python completes. Do not use them for long-running worker loops.
Use a `map` to pass named values to Python.
Each map key becomes a Python variable.
params = map('a', 10, 'b', 20, 'name', 'part'); result = python.run_code(""" result = name + ': ' + str(a + b) """, params); print(result); // part: 30
Expr values are converted to Python values. Python results are converted back to Expr values when possible.
Python dictionaries return as Expr map-like objects. Python lists return as array-like values.
info = python.run_code(""" result = { 'tool': 'T1', 'diameter': 6.0, 'feeds': [100, 200, 300] } """); print(info.tool); print(info.diameter); print(info.feeds.get(1)); // 200
This is useful when Python calculates several related values.
Use `python.run_file(filePath)` to run a Python file and wait for the result.
result = python.run_file('./scripts/calculate.py'); print(result);
Use `python.run_file(filePath, params)` to pass parameters.
result = python.run_file( './scripts/calculate.py', map('width', 40, 'height', 25) );
Python file paths use the same host path resolution rules as other Expr file paths. Use `./` for files inside the profile folder. See How relative paths work.
Use `python.code(code)` to create a `python_task` object. Call `run()` to start it.
task = python.code('result = 123'); task.run(); while (!task.completed) { } print(task.ok, ' ', task.result);
A `python_task` is configured first, then started once. Calling `run()` more than once raises an error.
Use asynchronous tasks when Python may take time or should communicate with Expr while it is running.
Use `on_complete(callback)` when you want a callback after the task finishes.
The callback receives two arguments: `ok` and `result`.
function Done(ok, result) { if (ok) { print('python result: ', result); } else { print('python failed'); } } task = python.code('result = 123').on_complete(Done); task.run();
Pass `Done`, not `Done()`.
Python code can send events to Expr with the `planetcnc` module.
task = python.code(""" import planetcnc planetcnc.send_event('status', 'started') planetcnc.send_event('value', 123) result = 'done' """); task.run(); while (!task.completed) { } event = task.read_event(); while (!event.is_none()) { print(event.name, ': ', event.value); event = task.read_event(); }
`read_event()` returns a map with `name` and `value`, or `none()` when no event is queued.
For high-rate events, read events regularly. The event queue is bounded.
Use `on_event(callback)` when you want Expr to receive events as they arrive.
The callback receives two arguments: event name and event value.
function Event(name, value) { print(name, ': ', value); } task = python.code(""" import planetcnc planetcnc.send_event('line', 'hello from python') result = 1 """).on_event(Event); task.run();
For GUI updates, a timer that polls `read_event()` can be easier to control because the GUI object, timer, and task can all be kept in one Expr object.
Long-running Python code can read commands from Expr with `planetcnc.read_command()`.
Expr sends commands with `python_task.send_command(name, value)`.
task = python.code(""" import planetcnc cmd = None while cmd is None: cmd = planetcnc.read_command() result = cmd['value'] + 1 """); task.run(); task.send_command('value', 41); while (!task.completed) { } print(task.result); // 42
`send_command()` returns `false` if the task has already completed.
Long-running Python code should check `planetcnc.terminate()` and exit cleanly.
Expr requests termination with `python_task.terminate()`.
task = python.code(""" import planetcnc import time while not planetcnc.terminate(): time.sleep(0.05) result = 'worker stopped' """); task.run(); task.terminate(); while (!task.completed) { } print(task.result);
This is cooperative termination. Python code must check `planetcnc.terminate()` regularly.
A worker task can run in Python, send events to Expr, and accept commands from Expr.
This example is a simplified version of a GUI worker pattern: Python produces text lines, Expr reads those events with a timer, and Expr can change Python mode with commands.
class PythonWorker() { task = none(); poll_timer = timer(this.Poll); done = false; function Code() { return """ import planetcnc import random import string import time mode = 'count' counter = 0 next_tick = time.monotonic() planetcnc.send_event('line', 'python worker started') while not planetcnc.terminate(): cmd = planetcnc.read_command() if cmd is not None: if cmd['name'] == 'mode': mode = cmd['value'] planetcnc.send_event('line', 'mode -> ' + str(mode)) now = time.monotonic() if now >= next_tick: if mode == 'count': counter += 1 planetcnc.send_event('line', 'count: ' + str(counter)) elif mode == 'rand': token = ''.join(random.choice(string.ascii_uppercase) for _ in range(8)) planetcnc.send_event('line', 'random: ' + token) next_tick = now + 0.5 time.sleep(0.05) result = 'worker stopped' """; } function Start() { done = false; task = python.code(this.Code()); task.run(); poll_timer.start(100); } function SetMode(mode) { if (!task.is_none()) { task.send_command('mode', mode); } } function Poll(count) { if (task.is_none()) { return; } event = task.read_event(); while (!event.is_none()) { if (event.name == 'line') { print(event.value); } event = task.read_event(); } if (!done && task.completed) { done = true; poll_timer.stop(); print('worker complete: ', task.ok, ', ', task.result); } } function Stop() { poll_timer.stop(); if (!task.is_none() && task.started && !task.completed) { task.terminate(); } } } worker = PythonWorker(); worker.Start(); worker.SetMode('rand'); // later worker.Stop();
The important parts are:
For GUI tools, keep the window, Python task, timer, and controls in one object.
This example starts a Python task from a button. Python sends progress lines to Expr, Expr displays them in a `listbox`, and the dialog can stop the task before it finishes.
class PythonProgressDialog() { wnd = none(); output = none(); start_button = none(); stop_button = none(); task = none(); poll_timer = timer(this.Poll); function WorkerCode() { return """ import planetcnc import time for i in range(1, 11): if planetcnc.terminate(): planetcnc.send_event('line', 'stopped by user') result = 'stopped' break planetcnc.send_event('line', 'step ' + str(i) + ' of 10') time.sleep(0.3) else: planetcnc.send_event('line', 'all steps complete') result = 'done' """; } function Show() { start_button = textbutton() .position(10, 10) .size(90, 28) .text('Start') .on_click(this.StartClicked); stop_button = textbutton() .position(110, 10) .size(90, 28) .text('Stop') .on_click(this.StopClicked); output = listbox() .position(10, 50) .size(360, 180); wnd = window() .title('Python Progress') .size(390, 280) .modal(false) .on_request(this.OnRequest); wnd.add(start_button); wnd.add(stop_button); wnd.add(output); wnd.show(); } function StartClicked() { if (!task.is_none() && task.started && !task.completed) { output.insert(0, 'worker is already running'); return; } output.insert(0, 'starting worker'); task = python.code(this.WorkerCode()); task.run(); poll_timer.start(100); } function StopClicked() { this.StopWorker(); } function Poll(count) { if (task.is_none()) { return; } event = task.read_event(); while (!event.is_none()) { if (event.name == 'line') { output.insert(0, event.value); } event = task.read_event(); } if (task.completed) { poll_timer.stop(); output.insert(0, 'result: ' + task.result.to_string()); } } function StopWorker() { poll_timer.stop(); if (!task.is_none() && task.started && !task.completed) { task.terminate(); poll_timer.start(100); } } function OnRequest(code) { this.StopWorker(); return true; } } tool = PythonProgressDialog(); tool.Show();
This pattern is useful because the dialog stays responsive while Python works. The timer only moves queued events into the UI and checks whether the task has completed.
The full worker example in your profile follows the same structure, with more controls and commands: window fields, button callbacks, a Python task, and a timer that polls worker events.
Using synchronous Python for a long-running worker:
python.run_code('while True: pass'); // wrong
Use `python.code(…)` and `python_task` for long-running work.
Forgetting to call `run()`:
task = python.code('result = 123'); // configured, not started yet task.run(); // starts the task
Expecting a task to run twice:
task.run(); task.run(); // error
Not checking for completion before using `result`:
task = python.code('result = 123'); task.run(); print(task.result); // may be too early
Better:
while (!task.completed) { } print(task.result);
Leaving a worker running when a dialog closes:
// Stop timer and terminate Python task in the dialog close/request callback.
Use synchronous Python for short calculations:
result = python.run_code('result = 40 + 2');
Use `python_task` for long-running or interactive work:
task = python.code(code); task.run(); // poll events, send commands, or use callbacks if (task.started && !task.completed) { task.terminate(); }
Keep long-running Python state in a class together with the task, timer, callbacks, and result values.