The Python ecosystem is rich in libraries for natural language processing. As a .NET developer, it can be frustrating to miss out on all the possibilities offered by the Python community. Hugging Face, Jupyter notebooks, and many resources may seem out of reach. Efforts have been made to address this gap - Semantic Kernel, Microsoft.Extensions.AI, ML.NET, etc. - but the Python ecosystem remains richer, more mature, and more popular.
What if you could use Python code directly in your .NET applications to access these powerful libraries? This is where CSnakes comes in. CSnakes is a .NET source generator and runtime that allows you to embed Python code and libraries into your C# .NET solution at a performant, low level - without the need for REST, HTTP, or microservices.
It supports .NET 8 and later, Python 3.9 up to 3.13, is cross-platform, maps Python types to C# types, and even leverages Span<T>
to access Python’s underlying memory directly. It can download Python at runtime - or reuse an existing Python installation - and install requirements from requirements.txt
using pip
.
Take this function that uses the transformers library to classify text with a zero-shot classification model:
import transformers
classifier = transformers.pipeline('zero-shot-classification', model='facebook/bart-large-mnli')
def classify(text: str, candidate_labels: list[str]) -> str:
result = classifier(text, candidate_labels)
return result['labels'][0]
Now, consider putting this code in a directory called python/
inside a C# console project. The requirements.txt
file was created after installing the transformers
package with pip
.
classifier/
ββ python/
β ββ classifier.py <-- Our Python method lives here
β ββ requirements.txt
ββ Classifier.csproj
ββ Program.cs
Next, install CSnakes in the C# project, mark the Python files as additional files accessible by CSnakes, and set requirements.txt
to be copied to the build output:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="python\classifier.py" CopyToOutputDirectory="PreserveNewest" />
<None Include="python\requirements.txt" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CSnakes.Runtime" Version="1.0.35" />
</ItemGroup>
</Project>
The CSnakes source generator will generate the C# code required to call the Python classify
function in Program.cs
:
using CSnakes.Runtime;
using CSnakes.Runtime.Locators;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
var pythonHomePath = Path.Join(AppContext.BaseDirectory, "python");
var pythonVenvPath = Path.Join(pythonHomePath, ".venv");
builder.Services
.WithPython()
.WithHome(pythonHomePath)
.FromRedistributable(RedistributablePythonVersion.Python3_12)
.WithVirtualEnvironment(pythonVenvPath)
.WithPipInstaller();
using var app = builder.Build();
var env = app.Services.GetRequiredService<IPythonEnvironment>();
var label = env.Classifier().Classify(
"One day I will see the world",
["travel", "cooking", "dancing"]);
Console.WriteLine(label); // Output: travel
You may notice the env.Classifier().Classify(...)
call. It comes from the generated code that CSnakes creates based on the Python function signatures:
// [...]
public static class ClassifierExtensions
{
private static IClassifier? instance;
private static ReadOnlySpan<byte> HotReloadHash => "a72fa94bdb19092b2f0aecf19a036c46"u8;
public static IClassifier Classifier(this IPythonEnvironment env)
{
if (instance is null)
{
instance = new ClassifierInternal(env.Logger);
}
Debug.Assert(!env.IsDisposed());
return instance;
}
// [...]
private class ClassifierInternal : IClassifier
{
private PyObject module;
private readonly ILogger<IPythonEnvironment>? logger;
private PyObject __func_classify;
internal ClassifierInternal(ILogger<IPythonEnvironment>? logger)
{
this.logger = logger;
using (GIL.Acquire())
{
logger?.LogDebug("Importing module {ModuleName}", "classifier");
this.module = ThisModule.Import();
this.__func_classify = module.GetAttr("classify");
}
}
// [...]
public string Classify(string text, IReadOnlyList<string> candidateLabels)
{
using (GIL.Acquire())
{
logger?.LogDebug("Invoking Python function: {FunctionName}", "classify");
PyObject __underlyingPythonFunc = this.__func_classify;
using PyObject text_pyObject = PyObject.From(text)!;
using PyObject candidateLabels_pyObject = PyObject.From(candidateLabels)!;
using PyObject __result_pyObject = __underlyingPythonFunc.Call(text_pyObject, candidateLabels_pyObject);
var __return = __result_pyObject.BareImportAs<string, global::CSnakes.Runtime.Python.PyObjectImporters.String>();
return __return;
}
}
}
}
Is this a good idea compared to hosting a Python service and calling it via HTTP? That’s up to you. There is some overhead in downloading Python and the classifier model at application startup. But CSnakes offers interesting possibilities for .NET developers who want to leverage the Python ecosystem without managing external services.
Watch this video Python Meets .NET: Building AI Solutions with Combined Strengths | BRK115 to learn more about CSnakes and its origin story.
The source code for this example is available on GitHub.