Thursday, June 19, 2025

Running Python Code within .NET Projects

Back home now after what was meant to be a relaxing holiday - unfortunately, it took quite a turn with some unexpected hospital stays both overseas and after coming home.

Started catching up and there has been some mind blowing announcements in the world of Software Development with AI.

For ML and AI, I think we all agree that Python is the go to programming language. Might change in the future, but at least currently that's the main one.

I recently had a requirement where we want to use a functionality that is written on Python to be consumed by a .NET application. The way I thought is to expose a Python API (using Flask, FastAPI etc), but then there is going to be a lot of data (floats) travelling through HTTP. Since I didn't have any other option, started on that.

And then on Microsoft Build 2025, there was this cool session on Python Meets .NET with Anthony Shaw and Scott Hanselman. It's a new project called CSnakes, where we could use Python using .NET and most importantly that's not by emulating or simulating Python. It's embedding Python into .NET process. .NET and Python code shares the same threads and same memory etc.
CSnakes
Let's have a look at a basic working example.

Here I have a basic Python function in a file called hello_world.py.
def say_hello(name: str) -> str:
    return f"Hello {name}!"
I have now created a Console Application that targets .NET 9 and installed CSnakes.Runtime package.
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="CSnakes.Runtime" Version="1.0.34" />
  </ItemGroup>

</Project>
I am putting the hello_world.py inside the project and updating .csproj file to copy the .py to Output directory. And this is a very important step, this enables CSnakes to run the source generator over Python files.
<ItemGroup>
  <AdditionalFiles Include="hello_world.py">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </AdditionalFiles>
</ItemGroup>
And now updating the Program.cs as follows.
using CSnakes.Runtime;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

IHostBuilder builder = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        // Path to your Python modules
        var home = Path.Join(Environment.CurrentDirectory, ".")
        services
            .WithPython()
            .WithHome(home)
            .FromRedistributable()// Download Python 3.12 and store it locally
    });

IHost app = builder.Build();

IPythonEnvironment pythonEnvironment =  app.Services.GetRequiredService<IPythonEnvironment>();

// IMPORTANT: Source generated by CSnakes
IHelloWorld helloWorld = pythonEnvironment.HelloWorld();
string result = helloWorld.SayHello("John Doe");
Console.WriteLine(result)// Hello John Doe!
Output
If you are wondering how IHelloWorld comes into the picture, it was generated at compile time. 
Generated Source
While it's the recommended approach, you can still use CSnakes without source generator.

Now let's see how an example of how to use a Python file that uses packages. We can make use of Python pip install and  requirements.txt .

First I am adding a requirements.txt file and enable copying it to the output.
<ItemGroup>
<AdditionalFiles Include="requirements.txt">
   <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </AdditionalFiles>
</ItemGroup>
Here for the demo purposes I am adding a simple package stringcase.
stringcase
Now I am modifying the hello_world.py file as follows.
import stringcase

def say_hello(name: str) -> str:
    return f"Hello {stringcase.titlecase(name)}!"
Now I am updating Program.cs the as follows.
using CSnakes.Runtime;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

IHostBuilder builder = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        // Path to your Python modules
        var home = Path.Join(Environment.CurrentDirectory, ".");
        services
            .WithPython()
            .WithHome(home)
            .FromRedistributable() // Download Python 3.12 and store it locally
            .WithVirtualEnvironment(Path.Join(home".venv"))
            .WithPipInstaller()// Optional, Installs packages listed in requirements.txt on startup
    });

IHost app = builder.Build();

IPythonEnvironment pythonEnvironment =
    app.Services.GetRequiredService<IPythonEnvironment>();

// Source generated by CSnakes
IHelloWorld helloWorld = pythonEnvironment.HelloWorld();
string result = helloWorld.SayHello("JohnDoe");
Console.WriteLine(result)// Hello John Doe!
Output
This is pretty cool!

Watch the video:

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment