pythonnet: Calling .NET Assemblies from Python, and Embedding Python in C#
Python for .NET is a package that gives Python programmers nearly seamless integration with the .NET Common Language Runtime (CLR) and provides a powerful application scripting tool for .NET developers.
At a glance
- What is it?
- Python.NET exposes CLR namespaces as importable Python packages and lets .NET applications host CPython. This review covers installation, the load() runtime switch, the GIL discipline required on the embedding side, and where the project stops being the right tool.
- Who is it for?
- Adopt pythonnet when you have existing .NET assemblies that Python must drive, or an existing CPython codebase that a .NET host must call, and you are prepared to keep every Python call inside a Py.GIL() block on the embedding side. Do not adopt it to replace C# with Python for new application code, and do not expect the README to cover packaging or troubleshooting, since it points to the Wiki for both.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 5 days ago.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 27, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The gap pythonnet fills: two runtimes, one process
CPython and the .NET CLR are separate virtual machines with separate object models, garbage collectors and type systems. Getting a C# library to answer a Python call normally means writing a C extension, a C ABI shim, or a subprocess protocol. pythonnet takes a different route: it loads the CLR into the Python process and maps CLR namespaces onto Python modules, so a .NET assembly looks like something you import.
The audience is narrow but real. On one side are Python developers who need a specific .NET library, often a Windows desktop or enterprise assembly with no Python equivalent. On the other side are .NET developers who want to script their application in Python, or reuse scientific Python from a C# host. The README frames it both ways: Python code interacting with the CLR, and Python embedded into a .NET application.
It is not a language bridge in the transpilation sense. Nothing is converted ahead of time and no code is generated. The two runtimes coexist, and every call crosses a boundary.
How the CLR becomes importable: clr, AddReference and clr_loader
The mechanism is a module named clr plus a loader dependency. The README states that CLR namespaces can be treated essentially as Python packages, so `from System.Collections import *` works directly. Assemblies that are not already loaded need an explicit `clr.AddReference("System.Windows.Forms")` before their namespaces resolve.
Runtime selection is the part worth reading twice. On Windows the default is .NET Framework, and on Linux and macOS it is Mono. .NET Core is used only if it is installed in a default location or the dotnet CLI is on PATH, and even then you must opt in, either with the environment variable `PYTHONNET_RUNTIME=coreclr` or by calling `pythonnet.load` explicitly before importing clr. That ordering matters: the load call has to happen first.
Runtime discovery itself lives in a separate distribution. pyproject.toml declares a single runtime dependency, `clr_loader>=0.3.1,<0.4.0`. The pin to the 0.3 series means pythonnet does not float on whatever loader version happens to be installed, which is a deliberate coupling rather than an oversight, but it does mean loader and pythonnet upgrades travel together.
The project declares `requires-python = ">=3.11, <3.16"` and classifiers for 3.11 through 3.15, on Windows, Linux and macOS. An interpreter outside that window is not supported by this release, regardless of whether it happens to import.
Installing pythonnet and making a first .NET call
The package is published to PyPI and to conda-forge; the README badges point at both, and project.urls lists the homepage at pythonnet.github.io. A plain pip install is the shortest path, and the README directs installation, FAQ and troubleshooting questions to the GitHub Wiki rather than covering them inline.
pip install pythonnetAfter installation, the first useful check is whether the CLR loads and whether you can reach a core namespace. The README's opening example is exactly this shape.
import clr
from System import String
from System.Collections import *If that runs without raising, the default runtime resolved. To force .NET Core instead of the Mono or .NET Framework default, call load before importing clr. The README gives this exact ordering.
from pythonnet import load
load("coreclr")
import clrThe third step is pulling in an assembly that is not part of the base runtime, using AddReference, and then importing a type from it.
import clr
clr.AddReference("System.Windows.Forms")
from System.Windows.Forms import FormWhat you should see is a Form class bound to the .NET type. The demo directory in the repository contains DynamicGrid.py, helloform.py, splitter.py and wordpad.py, which are worked examples of this same pattern against Windows Forms. The README does not document what happens when AddReference cannot locate an assembly, so name resolution failures are a Wiki question rather than a README one.
Embedding Python from C#: PythonDLL, the GIL and operator order
The reverse direction is where pythonnet imposes the most discipline. Since version 3.0 you must set the `Runtime.PythonDLL` property or the `PYTHONNET_PYDLL` environment variable before calling Initialize. The README is explicit that omitting it raises `BadPythonDllException`, an internal type derived from MissingMethodException, at Initialize time. Typical values are `python38.dll` on Windows, `libpython3.8.dylib` on macOS and `libpython3.8.so` on most Unix-like systems.
After Initialize, every call into Python must sit inside a `using (Py.GIL())` block. If Python objects will be touched from more than one thread, `PythonEngine.BeginAllowThreads()` is also required. Modules are imported with `dynamic mod = Py.Import("mod")` and then called like ordinary members. Python objects are declared as dynamic, and keyword arguments go through `Py.kw("name", value)` or the C# keyword-argument syntax.
One rule is easy to miss and produces wrong answers rather than exceptions: in mixed arithmetic, the Python object must come first. The README states that `np.pi * 2` works while `2 * np.pi` does not. That is a reflection of how the operator binding is resolved, and it means porting existing C# numeric code requires reordering expressions by hand.
The README's own example imports numpy, calls `np.cos(np.pi * 2)`, binds `np.sin` to a local, builds arrays with an explicit `dtype: np.int32`, and prints element-wise multiplication. The stated output is 1.0, then -0.958924274663, then -0.6752620892, then float64 and int32, then the array [ 6. 10. 12.]. Note that the example assumes numpy is available to the embedded interpreter.
Where pythonnet is the wrong tool
The GIL discipline is the sharpest limitation. Every Python call from .NET must be wrapped, and any code path that forgets the wrapper is a correctness bug, not a performance one. For a .NET service that calls Python rarely, that is manageable. For one that calls it on every request from many threads, the wrapping becomes a pervasive constraint on how the codebase is structured.
The runtime defaults are a second trap. A Linux or macOS deployment that assumes .NET Core will silently get Mono unless `PYTHONNET_RUNTIME=coreclr` is set or load is called. Code that works on a developer machine with the dotnet CLI on PATH can behave differently in a container where PATH differs.
Version support is bounded. The declared range is >=3.11, <3.16, so an older interpreter is out of scope for this release. And the README does not document rollback, unload of the CLR, or what happens if two assemblies conflict. If your requirement is process isolation or the ability to unload a runtime, an out-of-process design fits better than an in-process bridge.
Finally, pythonnet is not a way to avoid writing C#. If the target library has no Python binding and you only need a handful of functions, a small C# service behind an HTTP or gRPC boundary may be less work than managing a shared process, two garbage collectors and a GIL.
Alternatives and how they differ in approach
IronPython is the closest comparison and the most different underneath. It is a Python implementation running on the CLR, so Python code is compiled to .NET and there is no CPython interpreter in the process. That removes the GIL problem entirely and makes .NET calls native, but it also means CPython C extensions do not work. pythonnet keeps CPython and adds a CLR bridge, so numpy and other C extensions remain available, which is precisely why the README's example can import numpy at all.
ctypes sits at the opposite end. It calls C ABI functions from shared libraries with hand-written signatures and manual memory handling. There is no type mapping for managed objects and no namespace import, so calling a .NET assembly through ctypes means exporting a C entry point first. pythonnet does the marshalling for you, at the cost of a heavier runtime dependency.
The clr_loader dependency is worth naming separately here, because it is the component that actually finds and starts a runtime. It is a distinct distribution with its own version series, pinned to >=0.3.1,<0.4.0 in pyproject.toml, and it is what the load() call ultimately drives.
Maintenance, licensing and what a version bump costs
The repository is not archived. The last push was on 2026-09-20, and the most recent release is v3.2.0-rc1, tagged the same day. Before that came v3.1.0 on 2026-05-23 and v3.1.0-rc1 on 2026-05-16. The pattern is a release candidate followed by a stable release roughly a week later, with a few months between minor lines. The project is supported by the .NET Foundation, per the README.
Upgrade cost concentrates in two places. The clr_loader pin means a pythonnet major or minor bump can require a matching loader version, and the loader is the piece that touches runtime discovery, so a change there is where regressions would surface. The interpreter range moves with each release, so an upgrade can drop support for a Python version you still deploy.
Licensing is MIT for pythonnet itself. That is permissive, but MIT covers this project's code, not the .NET runtime, Mono, or any assembly you load through it, each of which carries its own terms. The repository does not enumerate those, so treat the licence question as one for your own review rather than something the README settles.
Editorial conclusion
Adopt pythonnet when you have existing .NET assemblies that Python must drive, or an existing CPython codebase that a .NET host must call, and you are prepared to keep every Python call inside a Py.GIL() block on the embedding side. Do not adopt it to replace C# with Python for new application code, and do not expect the README to cover packaging or troubleshooting, since it points to the Wiki for both. Verify first that your interpreter is inside the declared range (requires-python is >=3.11, <3.16) and that clr_loader>=0.3.1,<0.4.0 resolves cleanly, because that dependency is pinned to a minor series and is what actually locates the runtime.
Frequently asked questions
How do I install pythonnet?
Install it from PyPI with pip install pythonnet, or from conda-forge, which the README badges point to. The README directs installation details, FAQ and troubleshooting to the GitHub Wiki rather than covering them inline.
What is pythonnet used for?
It lets Python code interact with the .NET Common Language Runtime by treating CLR namespaces as Python packages, and it can also embed Python into a .NET application. The README describes it as integration for Python programmers and as an application scripting tool for .NET developers.
How do I use pythonnet to call a .NET assembly?
Import clr, call clr.AddReference with the assembly name, then import the type from its namespace. The README's example loads System.Windows.Forms and imports Form after the AddReference call, and the demo directory contains working scripts that follow the same pattern.
How does pythonnet compare with IronPython?
IronPython is a Python implementation that runs on the CLR, so Python code is compiled to .NET and CPython C extensions are unavailable. pythonnet keeps CPython in the process and adds a CLR bridge, which is why the README's embedding example can import numpy.
How does pythonnet differ from ctypes?
ctypes calls C ABI functions from shared libraries with hand-written signatures and manual memory handling, with no mapping for managed objects. pythonnet maps CLR namespaces onto Python modules and handles the marshalling, at the cost of a heavier runtime dependency.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/pythonnet-pythonnet)