Skip to main content

Invoking Python from C#

💡 PuerTS 3.0 also supports C# calling Javascript and Lua, each with different syntax. Click the links to see the corresponding tutorials.

Calling via Delegate​

PuerTS provides a key capability: converting Python functions into C# delegates. With this, you can call Python functions from the C# side.

public delegate void TestCallback(string msg);

public class TestClass
{
public TestCallback Callback;

public void TriggerCallback()
{
if (Callback != null)
{
Callback("hello_from_csharp");
}
}
}

void Start()
{
var env = new Puerts.ScriptEnv(new Puerts.BackendPython());
env.Eval(@"
exec('''
import Puerts.UnitTest.TestClass as TestClass
obj = TestClass()

def callback(msg):
global info
info = msg

# Assign a Python function to the C# delegate property
obj.Callback = callback
# Trigger the callback from C# side
obj.TriggerCallback()
''')
");
// info is now 'hello_from_csharp'
env.Dispose();
}

âš ī¸ Note: Multi-line Python code must be wrapped with exec('''...'''). Single-line expressions can be executed directly with Eval.

You can also directly invoke the delegate's Invoke method from the Python side:

# Directly invoke the delegate from Python
obj.Callback.Invoke('hello_from_python')

Passing Arguments from C# to Python​

When converting a Python function to a delegate, you can convert it to a delegate with parameters, allowing you to pass C# variables to Python. The type conversion rules are the same as when returning variables from C# to Python.

Python supports using lambda expressions to create simple anonymous functions:

void Start()
{
var env = new Puerts.ScriptEnv(new Puerts.BackendPython());
// Get a Python lambda as a C# delegate
System.Action<int> LogInt = env.Eval<System.Action<int>>("lambda a: print(a)");

LogInt(3); // Output: 3
env.Dispose();
}

For more complex logic, use def to define a function, then retrieve it via Eval:

void Start()
{
var env = new Puerts.ScriptEnv(new Puerts.BackendPython());
// Define a function with def, then retrieve it
env.Eval(@"
exec('''
def log_int(a):
print(a)
''')
");
System.Action<int> LogInt = env.Eval<System.Action<int>>("log_int");

LogInt(3); // Output: 3
env.Dispose();
}

Python functions also support optional parameters, which work correctly when converted to delegates with different signatures:

void Start()
{
var env = new Puerts.ScriptEnv(new Puerts.BackendPython());
env.Eval(@"
exec('''
def flexible_func(a, b=0):
if b == 0:
return str(a)
else:
return str(a) + str(b)
''')
");

// Cast as Action<int> — only pass the first argument
var cb1 = env.Eval<Action<int>>("flexible_func");
cb1(1); // Uses default b=0

// Cast as Action<string, long> — pass both arguments
var cb2 = env.Eval<Action<string, long>>("flexible_func");
cb2("hello", 999); // Output: hello999

env.Dispose();
}

Note: If your generated delegate has value type parameters, you need to add UsingAction or UsingFunc declarations. Please refer to the FAQ for details.


Calling Python from C# and Getting Return Values​

Similar to the previous section, just change the Action delegate to a Func delegate.

Using lambda expressions (suitable for simple one-line logic):

void Start()
{
var env = new Puerts.ScriptEnv(new Puerts.BackendPython());
// Python lambda can directly return a value
System.Func<int, int> Add3 = env.Eval<System.Func<int, int>>("lambda a: 3 + a");

System.Console.WriteLine(Add3(1)); // Output: 4
env.Dispose();
}

Using def to define functions (suitable for complex logic):

void Start()
{
var env = new Puerts.ScriptEnv(new Puerts.BackendPython());
env.Eval(@"
exec('''
def add3(a):
return 3 + a
''')
");
System.Func<int, int> Add3 = env.Eval<System.Func<int, int>>("add3");

System.Console.WriteLine(Add3(1)); // Output: 4
env.Dispose();
}

You can also use Eval<T> directly to get simple return values:

void Start()
{
var env = new Puerts.ScriptEnv(new Puerts.BackendPython());
// Directly evaluate a Python expression and get the return value
int result = env.Eval<int>("1 + 2");
System.Console.WriteLine(result); // Output: 3

string str = env.Eval<string>("'hello python'");
System.Console.WriteLine(str); // Output: hello python

// Convert non-string types with Python builtins
var ret = env.Eval<string>("str(9999)");
System.Console.WriteLine(ret); // Output: 9999

env.Dispose();
}

âš ī¸ Difference from Lua: Python's lambda expressions automatically return the result (similar to JS), without needing an explicit return. However, functions defined with def must use a return statement to return values, otherwise they return None.

Note: If your generated delegate has value type parameters, you need to add UsingAction or UsingFunc declarations. Please refer to the FAQ for details.


Error Handling in Python​

When Python code raises an exception using raise, the C# side can catch it with try-catch:

void Start()
{
var env = new Puerts.ScriptEnv(new Puerts.BackendPython());

// Python raise will be caught as a C# exception
try
{
env.Eval(@"
exec('''
raise Exception('something went wrong')
''')
");
}
catch (Exception e)
{
Debug.Log(e.Message); // Contains: something went wrong
}

// SyntaxError is also catchable
try
{
env.Eval(@"
exec('''
def test():
return 1 +
''')
");
}
catch (Exception e)
{
Debug.Log(e.Message); // Contains: SyntaxError
}

// RuntimeError (e.g. KeyError) is catchable too
try
{
env.Eval(@"
exec('''
obj = {}
obj['nonexistent']()
''')
");
}
catch (Exception e)
{
Debug.Log(e.Message); // Contains: KeyError
}

env.Dispose();
}

Environment Disposal and Delegate Lifecycle​

After the Python environment (ScriptEnv) is Dispose()d, previously converted delegates will no longer be usable. Calling a delegate from a disposed environment will throw an exception. Be sure to manage the lifecycle properly.

void Start()
{
var env = new Puerts.ScriptEnv(new Puerts.BackendPython());
System.Action callback = env.Eval<System.Action>("lambda: print('hello')");

callback(); // OK — Output: hello

env.Dispose();

// ❌ This will throw an exception!
// callback();
}

Implementing MonoBehaviour in Python​

Combining all the capabilities above, we can implement MonoBehaviour lifecycle callbacks in Python:

using System;
using Puerts;
using UnityEngine;

public class PythonBehaviour : MonoBehaviour
{
public Action PythonStart;
public Action PythonUpdate;
public Action PythonOnDestroy;

static ScriptEnv pythonEnv;

void Awake()
{
if (pythonEnv == null) pythonEnv = new ScriptEnv(new BackendPython());

pythonEnv.Eval(@"
exec('''
import UnityEngine.MonoBehaviour as MonoBehaviour

def init_behaviour(bindTo):
def on_update():
print(""update..."")
def on_destroy():
print(""onDestroy..."")
bindTo.PythonUpdate = on_update
bindTo.PythonOnDestroy = on_destroy
''')
");
var init = pythonEnv.Eval<Action<MonoBehaviour>>("init_behaviour");
if (init != null) init(this);
}

void Start()
{
if (PythonStart != null) PythonStart();
}

void Update()
{
if (PythonUpdate != null) PythonUpdate();
}

void OnDestroy()
{
if (PythonOnDestroy != null) PythonOnDestroy();
PythonStart = null;
PythonUpdate = null;
PythonOnDestroy = null;
}
}

âš ī¸ Key differences between Python and other languages:

  • Multi-line Python code requires exec('''...''') wrapping
  • Python uses def to define functions, no end or curly braces needed
  • Python uses import syntax to access C# types
  • Python's indentation is part of the syntax — keep it consistent

Key Differences Between Python and Other Languages for C# Invocation​

FeatureJavascriptLuaPython
Eval return valueLast expression value auto-returnedMust use returnlambda auto-returns; def needs return
Anonymous function(a) => { ... }function(a) ... endlambda a: ...
Named functionfunction f(a) { ... }function f(a) ... enddef f(a): ...
Multi-line codeWrite directlyWrite directlyWrap with exec('''...''')
Delegate assignmentobj.Callback = (msg) => { ... }obj.Callback = function(msg) ... endobj.Callback = callback_func
Method callDot obj.Method()Colon obj:Method()Dot obj.Method()
Console outputconsole.log()print()print()
Null valuenull / undefinednilNone
Throwing exceptionsthrow new Error()error()raise Exception()

Platform Limitations​

âš ī¸ The Python backend currently does not support WebGL, iOS, or Android platforms. If cross-platform support is needed, please use the Javascript or Lua backend.


📖 Other language tutorials for invoking from C#: C# to Javascript | C# to Lua | Multi-Language Comparison Cheat Sheet