Bridging Python and .NET: Hello CSnakes

Bridging Python and .NET: Hello CSnakes

·

7 min read

A few days ago, this post by Anthony Shaw popped up in my feed:

Here is his introductory blog if you are interested: https://tonybaloney.github.io/posts/embedding-python-in-dot-net-with-csnakes.html

Needless to say I was intrigued and I decided to play with it and then write about it. Two articles to be exact. The first one is an introductory one (this article you are reading) and the second one will be extending a previous project with CSnakes (Converting MNIST Data to CSV or Individual Files using .NET).

What is CSnakes?

I know that I will be repeating the documentation a bit but for completeness and reference it is a good idea to describe what this library is: CSnakes is a powerful library designed to seamlessly embed Python code into .NET projects. It leverages Python's C-API, and provides a high-performance way to invoke Python code directly from C# without needing intermediary layers like REST APIs or microservices.

This makes CSnakes an invaluable tool for developers who need the flexibility of Python's rich ecosystem within the robustness of the .NET environment and opens a lot of doors for .NET AI Developers.

Why Use CSnakes?

For many developers and mainly data engineers and scientists, Python is the go-to language. However, when it comes to deploying a production system, .NET often offers the necessary scalability and maintainability - not to mention Enterprise level applications. CSnakes bridges these two worlds, allowing you to take advantage of Python's powerful libraries within a .NET solution without sacrificing performance or ease of integration.

CSnakes generates C# classes from Python scripts which means as a .NET developer, you can access these classes just like any other class without needing external scripts, or APIs. It also means .NET developers can now access Python libraries like NumPy, Pandas, or even machine learning frameworks like TensorFlow directly in C#.

Getting Started with CSnakes

So, how do we use this library in our projects? It’s pretty straight forward actually. The steps are

  1. Install CSnakes Runtime

  2. Create or use Python Script

  3. Integrate This Python Script in C#

  4. Profit

Let’s take a look at this steps a bit more in detail:

1. Install CSnakes Runtime

First, I am going to create a CSnakesHelloWorld console application and add the CSnakes.Runtime package via NuGet in your .NET project:

dotnet add package CSnakes.Runtime

This package includes everything you need to embed Python code in your project.

2. Create a Python Script

Let’s create a simple Python script named hello.py that contains two functions - one to greet a user and the second one is a bit more statistics-like:

import statistics

def process_numbers(numbers: list[float]) -> dict[str, float]:
    result = {
        "mean": statistics.mean(numbers),
        "median": statistics.median(numbers),
        "stdev": statistics.stdev(numbers) if len(numbers) > 1 else 0.0
    }
    return result

def hello_world(name: str) -> str:
    return f"Hello, {name}!"

Add this script to your C# project directory and mark it for generation.

You can either do it in the .csproj file:

<ItemGroup>
        <AdditionalFiles Include="process_numbers.py">
            <CopyToOutputDirectory>Always</CopyToOutputDirectory>
        </AdditionalFiles>
    </ItemGroup>

Or click on the file in Visual Studio and change its properties as follows:

3. Integrate the Python Script in C#

Now, let’s integrate this Python script into our C# code.

When you add the following code, CSnakes will automatically generate a C# wrapper for this Python script, making it easy to invoke from .NET.

using CSnakes.Runtime;
using Microsoft.Extensions.Hosting;
using System.IO;
using System.Collections.Generic;

class Program
{
    static void Main(string[] args)
    {
        var builder = Host.CreateDefaultBuilder(args)
            .ConfigureServices(services =>
            {
                var home = Path.Combine(Environment.CurrentDirectory, "."); // Python module directory
                services
                    .WithPython()
                    .WithHome(home)
                    .FromNuGet("3.12.4"); // Specify Python version via NuGet
            });

        var app = builder.Build();
        var env = app.Services.GetRequiredService<IPythonEnvironment>();

And now, you can call your Python functions:

        // Invoke the Python function
        var numbers = new List<double> { 10.5, 20.0, 30.75, 40.25, 50.0 };
        var module = env.Hello();
        var res = module.HelloWorld("TJ");
        var result = module.ProcessNumbers(numbers);
        Console.WriteLine(res);
        Console.WriteLine($"Mean: {result["mean"]}, Median: {result["median"]}, " +
                          $"Std Dev: {result["stdev"]}");

As you can see, the name of our Python file becomes the name of the class and the functions in that file becomes the methods of our class.

Before we look under the hood let’s see the result:

4. Under The Hood

When you deep dive into the Hello class, this is what we find:

// <auto-generated/>
#nullable enable
using CSnakes.Runtime;
using CSnakes.Runtime.Python;

using System;
using System.Collections.Generic;
using System.Diagnostics;

using Microsoft.Extensions.Logging;

namespace CSnakes.Runtime;
public static class HelloExtensions
{
    private static IHello? instance;

    public static IHello Hello(this IPythonEnvironment env)
    {
        if (instance is null)
        {
            instance = new HelloInternal(env.Logger);
        }
        Debug.Assert(!env.IsDisposed());
        return instance;
    }

    private class HelloInternal : IHello
    {
        private readonly PyObject module;

        private readonly ILogger<IPythonEnvironment> logger;
        private readonly PyObject __func_process_numbers;
        private readonly PyObject __func_hello_world;

        internal HelloInternal(ILogger<IPythonEnvironment> logger)
        {
            this.logger = logger;
            using (GIL.Acquire())
            {
                logger.LogDebug("Importing module {ModuleName}", "hello");
                module = Import.ImportModule("hello");
                this.__func_process_numbers = module.GetAttr("process_numbers");
                this.__func_hello_world = module.GetAttr("hello_world");
            }
        }

        public void Dispose()
        {
            logger.LogDebug("Disposing module {ModuleName}", "hello");
            this.__func_process_numbers.Dispose();
            this.__func_hello_world.Dispose();
            module.Dispose();
        }

        public IReadOnlyDictionary<string, double> ProcessNumbers(IReadOnlyList<double> numbers)
        {
            using (GIL.Acquire())
            {
                logger.LogDebug("Invoking Python function: {FunctionName}", "process_numbers");
                PyObject __underlyingPythonFunc = this.__func_process_numbers;
                using PyObject numbers_pyObject = PyObject.From(numbers)!;
                using PyObject __result_pyObject = __underlyingPythonFunc.Call(numbers_pyObject);
                return __result_pyObject.As<IReadOnlyDictionary<string, double>>();
            }
        }
        public string HelloWorld(string name)
        {
            using (GIL.Acquire())
            {
                logger.LogDebug("Invoking Python function: {FunctionName}", "hello_world");
                PyObject __underlyingPythonFunc = this.__func_hello_world;
                using PyObject name_pyObject = PyObject.From(name)!;
                using PyObject __result_pyObject = __underlyingPythonFunc.Call(name_pyObject);
                return __result_pyObject.As<string>();
            }
        }
    }
}
public interface IHello : IDisposable
{
    IReadOnlyDictionary<string, double> ProcessNumbers(IReadOnlyList<double> numbers);
    string HelloWorld(string name);
}

The generated class above basically generates a static class for the Python functions in our file. The code uses a Global Interpreter Lock (GIL) to ensure thread-safe execution of Python code.

The #nullable enable directive at the top indicates that nullable reference types are enabled, which is a C# feature to help prevent null reference exceptions.

One might ask how this library is different to Iron Python. Let’s see what they say on their FAQ section:

IronPython is a .NET implementation of Python that runs on the .NET runtime. CSnakes is a tool that allows you to embed Python code and libraries into your C#.NET solution. CSnakes uses the Python C-API to invoke Python code directly from the .NET process, whereas IronPython is a separate implementation of Python that runs on the .NET runtime.

Bonus: Calling Complex Python Libraries

The power of CSnakes really shines when using Python's rich data science libraries. You can easily integrate libraries like Pandas, NumPy, or Scikit-Learn without leaving the .NET ecosystem. For instance, running a data transformation using Pandas can be done seamlessly from C#.

For example, I added the code below to the hello.py file;

import pandas as pd

def summarize_data(csv_path: str) -> str:
    df = pd.read_csv(csv_path)
    return df.describe().to_string()

Then I reran the generator by going into the generated class and clicking on the button at the top:

Once done, you will see that the new method is included in the class and we can now call it just like the others.

var result = module.SummarizeData(@"C:\MyPath");

This Python function can be called from C# using the same principles, providing easy access to Python's powerful data processing capabilities.

💡
You can use more than one Python files, however, do not forget to mark them for inclusion. Otherwise, the C# classes won’t be generated.

You also do not need to copy the Python files in the root directory. You can easily create a folder and tell the engine to process them all. Take a look at this example below:

Just a heads up → you need to edit the .csproj file for this to work.


You can find the source code to the project here: https://github.com/tjgokken/CSnakesHelloWorld

Conclusion

CSnakes is a powerful bridge between the world of Python and .NET, allowing developers to combine the best of both ecosystems. Whether you need the flexibility of Python for data science tasks or the reliability and scalability of .NET, CSnakes allows you to harness both in a seamless, high-performance way.

If you're looking to enhance your .NET projects with Python's vast library ecosystem, CSnakes is the perfect tool to get you there—reducing complexity, increasing performance, and giving you the best of both worlds.