So I definitely don’t do everything using C# and .NET. I also use Python, but sometimes I just do not want another tiny Python service.

I want one Python library inside an otherwise normal .NET app. No container boundary. No HTTP wrapper. No extra deployment for something that is really just a function call.

CSnakes is interesting for that case. It runs Python in the same process and generates C# bindings from typed Python functions. The Python code stays in Python, but the .NET side can call it through a normal-looking method.

That makes it a good fit for quick demos, proofs of concept, internal tools, or the one feature where Python has exactly the library you need.

The shape

Install the packages:

dotnet add package CSnakes.Runtime
dotnet add package Microsoft.Extensions.Hosting

The project file shape is still small. Add the package references and include your Python files as analyzer additional files. That is how the source generator finds them:

<ItemGroup>
  <PackageReference Include="CSnakes.Runtime" Version="1.2.1" />
  <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
</ItemGroup>

<ItemGroup>
  <AdditionalFiles Include="python/*.py" SourceItemType="Python">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </AdditionalFiles>
  <None Include="requirements.txt" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

Then wire up Python at startup:

using CSnakes.Runtime;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);
var pythonHome = Path.Join(AppContext.BaseDirectory, "python");

builder.Services
    .WithPython()
    .WithHome(pythonHome)
    .FromRedistributable();

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

FromRedistributable() can download and use an isolated Python runtime. If your environment already standardizes on Conda or system Python, CSnakes can use those too.

A small example

Imagine a .NET app that needs one fuzzy matching function from Python. That is small enough that a separate API would feel silly.

Add a Python dependency in requirements.txt:

rapidfuzz

For a real app, pin the package version. Treat it like any other dependency you care about.

We can update our startup code to use CSnakes’ uv integration to handle this automatically:

var requirements = Path.Join(AppContext.BaseDirectory, "requirements.txt");

builder.Services
    .WithPython()
    .WithHome(pythonHome)
    .FromRedistributable()
    .WithVirtualEnvironment(Path.Join(AppContext.BaseDirectory, ".venv"))
    .WithUvInstaller(requirements);

The first run may need network access because CSnakes can download the redistributable Python runtime and uv installs the packages from requirements.txt.

Now add python/text_tools.py:

from rapidfuzz import fuzz

def match_score(left: str, right: str) -> float:
    return float(fuzz.token_set_ratio(left, right))

Because the function has type hints, CSnakes can generate a typed C# wrapper. Python’s match_score shows up as MatchScore in C#.

var textTools = env.TextTools();

var score = textTools.MatchScore(
    "Azure OpenAI invoice processing",
    "invoice processing with Azure AI");

Console.WriteLine(score);

The .NET code does not shell out to python. It also does not serialize an HTTP request just to call one local function.

What to keep in mind

Keep the Python boundary boring.

I would expose a few typed functions like match_score, extract_features, or rank_candidates. I would not let half the domain model move into Python because it was convenient in the moment.

Also remember that this is in-process. That is the point, but it changes the failure model. A Python dependency problem is now your application startup problem. A heavy Python call can affect your .NET process. The Python environment needs the same care as the rest of your runtime.

Deployment is the other boundary to check early. CSnakes can work with Native AOT when you use the source-generated bindings shown above, but not every interop path is AOT friendly. Manual Python binding still runs into the usual AOT limits around reflection and dynamic type handling. A self-contained .NET publish also does not magically include Python, your packages, or the .venv directory. Those still need to be bundled with the app.

When to use it

I would reach for CSnakes when:

  • the .NET app only needs a small slice of Python
  • HTTP would add more ceremony than value
  • the Python function has clear typed inputs and outputs
  • the integration belongs inside the app, not beside it

I would still use a separate Python service when the Python side has its own scaling needs, GPU runtime, deployment lifecycle, or team ownership. In-process interop is useful, but it is not a replacement for a real service boundary when you actually need one.

Further reading