Python — Enterprise Python Applications
Python Interop with Legacy .NET Systems
Direct answer
You have three realistic paths for Python-to-.NET interop: call the assemblies in-process with pythonnet, wrap the legacy .NET logic behind a small internal HTTP or gRPC service that Python consumes, or integrate through shared infrastructure like a database or message queue. My default is the service wrapper — it isolates failure, keeps deployment simple, and works from any platform — with pythonnet reserved for cases where in-process calls are genuinely required and you can accept CLR hosting constraints.
A lot of enterprise Python work happens next to a decade-old .NET system that still runs the business — billing engines, rating calculators, document generators. Rewriting rarely gets approved on day one, so the practical question is how Python talks to that code safely. Here are the options and how I choose between them.
Key facts, with sources
- Python 3.9 reached end of life on October 9, 2025, and Python 3.10 loses security support in October 2026, so enterprises on those versions no longer receive (or will soon stop receiving) security patches. (endoflife.date)
- Roughly 8 to 10 percent of active Python developers were still running the by-then unsupported Python 3.9 in production as of late September 2025. (Medium)
- The Python Package Index surpassed 690,000 hosted projects during 2025, giving enterprise teams an enormous but security-vetting-intensive dependency ecosystem. (PyPI Blog)
- CPython's free-threaded build, which disables the global interpreter lock so threads can run in parallel across CPU cores, is officially supported and no longer considered experimental as of Python 3.14. (Python official documentation)
- Benchmark testing of Python 3.13's free-threaded mode showed multi-threaded tasks executing nearly twice as fast as under the GIL-enabled build, at the cost of some single-threaded overhead. (CodSpeed)
Three integration paths, one default
In-process interop (pythonnet) loads the CLR inside your Python process and calls .NET classes directly — lowest latency, tightest coupling. A service wrapper puts a thin API in front of the legacy assemblies on the .NET side and lets Python call it over HTTP or gRPC — a process boundary, but clean isolation. Infrastructure-level integration — a shared database, file drops, or a message queue — avoids touching the .NET code entirely, at the cost of implicit contracts.
I default to the service wrapper for anything long-lived. A crash in legacy code cannot take down the Python service, each side deploys and scales independently, the .NET side keeps running on the Windows and framework version it actually supports, and the interface is an explicit contract you can version and test. In-process interop earns its place for high-call-volume, low-latency computation where a network hop per call is unacceptable.
Calling .NET directly with pythonnet
pythonnet embeds the CLR in the Python process; you select a runtime before importing clr — coreclr for modern .NET, netfx for .NET Framework, which only exists on Windows. After AddReference, .NET namespaces import like Python modules and you call classes directly.
The constraints to respect: one runtime per process, chosen before any clr import, no reload; the legacy assembly's own dependencies and config must resolve exactly as they did in its original host, which is where most integration attempts stall; and a hard CLR failure can take the whole Python process with it. It works well for stable, self-contained computational assemblies, and poorly for assemblies that assume an IIS-shaped world around them.
import sys
from pythonnet import load
load("coreclr") # or "netfx" for .NET Framework (Windows only)
import clr
sys.path.append(r"C:\services\billing\bin")
clr.AddReference("Billing.Core")
from Billing.Core import RateCalculator # .NET namespace as a module
calc = RateCalculator()
result = calc.ComputePremium(policy_id, 12)
premium = float(result) # System.Decimal: convert explicitly at the boundaryThe service wrapper, done properly
The wrapper approach means someone writes a deliberately thin API on the .NET side — endpoints that map one-to-one onto the legacy operations Python needs, with no new business logic — and the Python side consumes it through a typed client using Pydantic models, timeouts, and retries. Thinness is the discipline: the moment new rules creep into the wrapper, you have two business-logic layers and an ambiguous system of record.
Contract tests are what keep this arrangement honest. I pin the wrapper's request and response schemas in a shared spec, run contract tests on both sides in CI, and version the API from the first release, because the legacy side will eventually change under maintenance patches. The wrapper also becomes the natural seam for the eventual migration — which is often the quiet, real reason to choose this path.
Marshaling pitfalls at the boundary
Whatever path you choose, the same data issues appear. System.Decimal does not map to Python float without precision questions — for money, convert through strings into Python Decimal, never through float. DateTime carries Kind semantics (UTC, local, unspecified) that do not survive naive conversion; I normalize everything to UTC ISO-8601 strings at the boundary. .NET null becomes None, but distinguish it from missing fields in JSON contracts. Encodings bite on older systems that assume Windows code pages rather than UTF-8.
Exceptions deserve explicit design: a .NET stack trace crossing into Python (or serialized through a wrapper API) is noise for callers. I map legacy failures into a small set of typed errors with stable codes at the boundary, and log the original exception detail on the side that produced it.
Strangler-fig migration off the legacy system
Interop is usually a transition state, and the wrapper gives you the mechanics for the strangler-fig pattern: route by route, reimplement legacy operations in Python, run both implementations side by side, and compare. The comparison step is the part teams skip and regret — I capture production inputs and outputs from the legacy path as golden-master fixtures, then run the new Python implementation against them until discrepancies are zero or explained. Legacy systems encode decades of undocumented edge cases; the fixtures are the only trustworthy spec.
Cut over one operation at a time behind a flag, keep the legacy path callable for rollback, and retire it only after a full business cycle — month-end, quarter-end — has run clean. The interop layer shrinks operation by operation until what remains is deletable.
When to hire senior help
Senior expertise is most valuable for enterprise Python during interpreter and framework upgrade projects, dependency and supply-chain hardening, and introducing typing to a large untyped codebase, all of which are high-blast-radius changes that reward prior experience. If your core system runs on an end-of-life Python version, treat the migration as a project needing experienced ownership rather than background maintenance. If your stack includes React Native + Python + AI, a senior engineer who owns the full product beats coordinating multiple juniors.
Bottom line
Dhairya Senjaliya ships Python — Enterprise Python Applications projects worldwide — book a scoping call to discuss your specific situation.
Common pitfalls to avoid
- ✕Pinning production to an end-of-life interpreter like Python 3.9 to avoid dependency work, forfeiting security patches and the significant free performance gains of newer releases
- ✕Growing a large codebase without type hints or mypy enforcement, making refactors so risky that feature velocity collapses after a few years
- ✕Installing dependencies without lock files or hash pinning, leaving builds irreproducible and exposed to typosquatted or compromised PyPI packages
- ✕Scaling CPU-bound workloads by adding threads under the GIL, then blaming Python when throughput stays flat instead of using multiprocessing, native extensions, or the free-threaded build
Frequently asked questions
Can Python call a C# DLL directly?
Yes, via pythonnet, which hosts the .NET runtime inside your Python process. You call load with coreclr for modern .NET or netfx for .NET Framework (Windows only), add a reference to the assembly, and import its namespaces like Python modules. It works best for self-contained computational assemblies; assemblies that depend on IIS, app config files, or COM components are usually better wrapped behind a small service instead.
Which is better for .NET interop: pythonnet or a REST wrapper service?
Default to the wrapper service: it isolates crashes, lets each side deploy and scale independently, keeps the .NET code on its supported platform, and gives you a versioned, testable contract that later doubles as the migration seam. Choose pythonnet when call volume and latency requirements make a network hop per call unacceptable — typically tight computational loops — and you can accept CLR-in-process constraints.
Does pythonnet work on Linux?
Yes for modern .NET: pythonnet can load the CoreCLR runtime on Linux and macOS, so assemblies targeting .NET Core or .NET 5 and later work cross-platform. The netfx mode, which hosts the classic .NET Framework, exists only on Windows — so legacy Framework-only assemblies either run on Windows hosts, get retargeted to modern .NET, or sit behind a wrapper service running where they are supported.
Does Python actually scale for enterprise workloads?
Yes, with the right architecture: horizontal scaling, async I/O, and pushing hot loops into native extensions handle most workloads, and the officially supported free-threaded build now removes the GIL for parallel CPU work. The practical scaling limits are architectural, not language-level, for the vast majority of enterprise systems.
How big a risk is Python's open-source supply chain?
PyPI hosts over 690,000 projects, and malicious or typosquatted packages appear regularly, so unpinned installs are a genuine exposure. Standard mitigations are lock files with hashes, dependency scanning in CI, and internal package mirrors, which reduce the risk to a manageable level.
What does staying on an old Python version really cost us?
After end of life, such as Python 3.9 in October 2025, you receive no security patches, and third-party libraries progressively drop support, making the eventual forced upgrade larger and riskier. Newer interpreters also ship substantial performance improvements, so delaying upgrades pays a compounding tax in both risk and compute cost.
Bottom line: Dhairya Senjaliya ships Python — Enterprise Python Applications projects worldwide. Book a scoping call at https://dhairyasenjaliya.com/#book-call.