4/28/2025 Alan Robertson

Building a Dynamic Python Execution Environment with .NET MAUI, Pyodide, Monaco, and Radzen


Introduction and Overview

image

In this blog post, we will explore how to build a dynamic Python execution environment using .NET MAUI. This project allows users to write Python code in a Monaco Editor, execute it in the browser using Pyodide, and display the results in a Radzen DataGrid. The application is designed to provide a seamless experience for running Python scripts, with a focus on data manipulation using Pandas.

Key features of this project include:

  • A rich code editor powered by Monaco.
  • Python execution in the browser using Pyodide.
  • Data visualization using Radzen DataGrid.
  • User-friendly error handling for Python execution.

Requirements and Setup

To get started, ensure you have the following prerequisites:

  • .NET 9 SDK: Required to build and run the .NET MAUI project.
  • Modern Web Browser: Pyodide runs in the browser, so a modern browser is essential.
  • Radzen Blazor Components: Used for UI elements like buttons and data grids.

Clone the repository and set up the project:

  1. Clone the repository: https://github.com/BlazorData-Net/DynamicPythonDotNet/tree/main
  2. Build and run the project

Implementing Pyodide

Pyodide is a Python runtime for the browser, enabling Python code execution in a web environment. In this project, Pyodide is used to execute Python scripts and return results in JSON format.
The implementation is defined in index.html:

<script src="https://cdn.jsdelivr.net/pyodide/v0.27.1/full/pyodide.js"></script>
<script>
    let pyodideReadyPromise = loadPyodide();
    async function runPythonScript(pythonCode) {
    let pyodide = await pyodideReadyPromise;
    // Load the Pandas package
    await pyodide.loadPackage("pandas");
    // Import necessary modules
    pyodide.runPython(`import json`);
    try {
        // Execute the Python code
        pyodide.runPython(pythonCode + `
_result = load_data()
if not isinstance(_result, pd.DataFrame):
    raise TypeError("Must return a DataFrame")
_result_json = _result.to_json(orient="records")
`);
        // Retrieve the JSON result
        const resultJson = pyodide.globals.get('_result_json');
        return resultJson;
    } catch (error) {
        // Extract and throw only the relevant error message
        const errorMessage = error.message.split("\n").find(line => line.includes("TypeError: Must return a DataFrame"));
        throw new Error(errorMessage || "An error occurred while executing the Python code.");
    }
}
</script>

 

Key points:

  • Pyodide is initialized using loadPyodide().
  • The runPythonScript function executes Python code and ensures the result is a Pandas DataFrame.
  • Errors are handled gracefully, and only relevant error messages are displayed.

Implementing Monaco

The Monaco Editor provides a rich code editing experience with syntax highlighting for Python. In this project, the editor is integrated into the Home.razor file.

 

<CodeEditor @ref="@MonacoCodeEditor"
            Language="python"
            Code="@CurrentScript"
            Height="300px"
            Width="100%" />

 

Key features:

  • The CodeEditor component is used to embed the Monaco Editor.
  • The Language property is set to "python" for Python syntax highlighting.
  • The Height and Width properties define the editor's dimensions.

The CurrentScript variable is used to manage the code in the editor. A sample script is preloaded when the page initializes:

 

protected override void OnInitialized()
{
    CurrentScript = SampleScript;
}

Executing the Python Code and Displaying Results in Radzen DataGrid

The ExecutePython method in Home.razor handles the execution of Python code and displays the results in a Radzen DataGrid.

 

private async Task ExecutePython()
{
    try
    {
        Message = ""; // Clear any previous error messages
        isRunning = true;
        StateHasChanged();
        // Get Python code from Monaco
        var pythonCode = await MonacoCodeEditor.GetCodeAsync();
        // Call JS to execute Python via Pyodide and get JSON
        var json = await JSRuntime.InvokeAsync<string>("runPythonScript", pythonCode);
        // Parse and flatten JSON into a table-friendly format
        dataRows = new List<Dictionary<string, object>>();
        var parsedArray = JsonNode.Parse(json)?.AsArray();
        if (parsedArray != null)
        {
            foreach (var item in parsedArray)
            {
                var dict = new Dictionary<string, object>();
                if (item is JsonObject obj)
                {
                    foreach (var prop in obj)
                    {
                        dict[prop.Key] = prop.Value?.GetValue<object>() ?? "";
                    }
                }
                dataRows.Add(dict);
            }
        }
    }
    catch (JSException jsEx)
    {
        // Check if the error message contains "Must return a DataFrame"
        if (jsEx.Message.Contains("Must return a DataFrame"))
        {
            // Display a custom error message
            Message = "The Python code must return a valid Pandas DataFrame. Please ensure your function returns a DataFrame.";
        }
        else
        {
            // Display the original error message for other errors
            Message = jsEx.Message;
        }
    }
    catch (Exception ex)
    {
        // Handle other errors
        Message = ex.Message;
    }
    finally
    {
        isRunning = false;
        StateHasChanged();
    }
}

Key points:

  • The Python code is retrieved from the Monaco Editor using MonacoCodeEditor.GetCodeAsync().
  • The runPythonScript JavaScript function is invoked to execute the code.
  • The JSON result is parsed and displayed in a Radzen DataGrid.

The Radzen DataGrid is defined as follows:

 

@if (dataRows?.Any() == true)
{
    <RadzenDataGrid Data="@dataRows" TItem="Dictionary<string, object>">
        <Columns>
            @foreach (var key in dataRows[0].Keys)
            {
                <RadzenDataGridColumn TItem="Dictionary<string, object>" Title="@key">
                    <Template Context="context">
                        @context[key]
                    </Template>
                </RadzenDataGridColumn>
            }
        </Columns>
    </RadzenDataGrid>
}

 

Conclusion

This project demonstrates how to integrate Pyodide, Monaco, and Radzen into a .NET MAUI application to create a dynamic Python execution environment. By combining these technologies, you can build powerful tools for data analysis and visualization directly in the browser.

An unhandled error has occurred. Reload 🗙