.NET & Python

Calling Python from a .NET Application (and When You Actually Should)

Your app is .NET, but the library — or the model — you need is Python. Here are the integration options, honestly compared, and the decision that comes first: whether you should at all.

It's a common crossroads. Your application is built in .NET — solid, typed, fast — but the thing you now need lives in Python: a machine-learning model your data team trained, a mature library with no real .NET equivalent, or a script that already works and nobody wants to rewrite. So the question lands: how do you call Python from a .NET application — and, just as important, should you?

There are several ways to bridge the two, from a one-line subprocess call to a full microservice, each with very different trade-offs. But the integration choice is the second question. The first one decides whether you should introduce Python into your stack at all.

First: should you actually reach for Python?

Adding a second language to a system is not free. Before you build any bridge, it's worth being honest about whether the bridge should exist. There are good reasons — and some bad ones.

Good reasons to call Python:

  • A mature library with no .NET equivalent. The data-science and ML ecosystem — pandas, scikit-learn, PyTorch, spaCy, countless research libraries — genuinely lives in Python. Reimplementing it in C# would be a project of its own.
  • A model your data team already trained. The people who build the model work in Python. Handing you a .pkl or a saved model and a few lines of inference code is the natural handoff — far better than translating their work by hand.
  • An existing, working script. There's a Python tool that already does the job correctly. Reusing it beats rewriting and re-testing it in another language.

When to not reach for Python:

  • A capable .NET option already exists. For plenty of tasks — including a lot of ML — ML.NET, Math.NET, or a NuGet package does the job in-process, with none of the cross-language cost. Check first.
  • The logic is trivial. Standing up a second runtime to run twenty lines of arithmetic is a poor trade. Just write it in C#.
  • You're doing it for taste, not need. "I prefer Python for this" isn't a reason to split a production stack. The team that maintains it inherits two ecosystems forever.

The honest test: you should cross the language boundary when Python gives you something real that .NET genuinely can't — usually a model or a specialized library — and the value clearly outweighs the operational cost of running two runtimes. If it does, here's how.

Option 1 — Shell out to a Python script

The simplest bridge: run Python as a separate process from .NET, pass data in (arguments, standard input, or a file), and read the result back from standard output. It's Process.Start and a little plumbing — no extra frameworks.

ShellOut.cs
// Run a Python script as a subprocess; pipe JSON in, read the result out
var psi = new ProcessStartInfo {
    FileName = "python",
    Arguments = "score.py",
    RedirectStandardInput  = true,
    RedirectStandardOutput = true,
    UseShellExecute = false
};
using var proc = Process.Start(psi);
await proc.StandardInput.WriteAsync(inputJson);   // send data in
proc.StandardInput.Close();
var result = await proc.StandardOutput.ReadToEndAsync();  // read result out

Great when: the work is batch or offline, runs occasionally, and latency doesn't matter — a nightly job, a data transform, a one-shot inference. The costs: you pay process-startup time on every call, you're serializing data across the boundary (usually JSON), and error handling is crude — a non-zero exit code and whatever landed on stderr. It's perfectly good for the right job, and a poor fit for anything high-volume or latency-sensitive.

Option 2 — Wrap Python in a small web service (our usual pick)

For anything that runs in production and gets called repeatedly, the cleanest answer is to stop thinking of it as "calling Python" and treat it as calling a service that happens to be written in Python. Wrap the model or library in a tiny FastAPI (or Flask, or gRPC) app, load the model once at startup, and expose one endpoint. Your .NET app calls it over HTTP like any other dependency.

model_service.py
# model_service.py — wrap the Python model behind a tiny HTTP API
from fastapi import FastAPI
from pydantic import BaseModel
import joblib

app = FastAPI()
model = joblib.load("model.pkl")   # load once at startup, not per request

class Input(BaseModel):
    text: str

@app.post("/score")
def score(item: Input):
    prediction = model.predict([item.text])
    return { "score": float(prediction[0]) }

From .NET it's a plain HttpClient call — await http.PostAsJsonAsync("/score", input) — deserialized into a typed result. The language boundary becomes a network boundary, which turns out to be a feature: the two sides deploy, scale, and fail independently. If the model is heavy, you scale just the Python service. If it falls over, your .NET app degrades gracefully instead of crashing.

Great when: it's a production, keeps-running workload — especially model inference behind a user-facing feature. It's the same shape as any AI integration we put into production, and it's the default we reach for. The cost: one more deployable service to run, monitor, and secure, plus the network hop's latency (usually negligible on the same host or network).

Option 3 — In-process interop (Python.NET), with care

You can also run Python inside your .NET process using Python.NET (the pythonnet package) or the newer CSnakes — calling Python objects directly from C#, no serialization, no network hop. The upside is the lowest possible latency and the tightest coupling.

The upside is also the downside: tight coupling. The Python runtime now lives inside your app, so the two are welded together — you deploy a matching Python interpreter and its packages alongside your .NET app, you inherit Python's Global Interpreter Lock (a real constraint under concurrency), and a bad dependency or a segfault on the Python side can take your whole process down. It's the right tool when you genuinely need per-call latency that a network hop can't give you — a hot path calling Python many times per request. For most teams, that bar is higher than they expect, and the service boundary in Option 2 is the safer default.

A note on the async/batch case: a queue

If the Python work is long-running or bursty and doesn't need an immediate answer — generating a report, scoring a batch, processing an upload — don't make .NET wait on it at all. Drop a message on a queue (SQS, RabbitMQ, Azure Service Bus) and let a Python worker consume it. It's the most decoupled option of all: the two sides don't even have to be up at the same time, and each scales on its own. This is the same pattern behind a lot of Python data and reporting pipelines — the request just kicks off work that lands somewhere later.

The part teams underestimate: the operational cost

Whichever bridge you pick, the moment Python enters a .NET system you own two of everything: two runtimes to install and patch, two dependency ecosystems (NuGet and pip) to keep current and secure, two sets of build and deploy steps, and monitoring that has to span both. None of that is a reason not to do it — it's just the real price of the capability, and it belongs in the decision up front, not as a surprise in month three. A well-chosen boundary (usually the service in Option 2) keeps that cost contained; an in-process entanglement spreads it everywhere.

How we'd approach it

Our default is boring on purpose: if the Python is doing something real that .NET can't, wrap it behind a small service and call it over HTTP. Reach for a plain subprocess when the job is simple and offline, and a queue when it's async or batch. Keep Python in-process only when measured latency genuinely demands it — and know what you're signing up for when you do.

The thread through all of it: the integration is the easy part. The judgment is knowing whether to cross the boundary at all, and then choosing the loosest coupling that still meets the requirement. That mix — modern .NET and a strong Python and data practice under one roof — is exactly the kind of cross-stack work we do. If you've got a .NET app and a Python model (or the reverse) and you're weighing how to connect them, tell us what each side needs to do and we'll help you pick the boundary — then build it.

Let's Talk About Your Project

A quick 30‑minute call is all it takes to find out if we're a good fit for each other. Book a time and we'll take it from there.

Book a Call