Using .NET from Python

SteelPython runs on .NET, so the whole .NET class library - and any .NET assembly you have - is available to Python code, in the same process, with no bridge to set up: .NET namespaces import like Python packages. If you have used pythonnet with CPython, this will feel familiar, without its import clr and clr.AddReference ceremony.

Importing .NET namespaces

from System import Math, Environment
from System.Collections.Generic import List, Dictionary

print(Math.Max(3, 7), Math.Round(2.567, 2))  # 7 2.57
print(Environment.ProcessorCount)

A .NET namespace imports like a Python package, with nothing to set up first. Python modules always win: if a folder or module on sys.path has the same name as a namespace, import finds the Python one.

Every namespace of the .NET runtime is importable, whether or not its assembly has been loaded yet: from System.Collections.ObjectModel import ObservableCollection loads the assembly that defines it.

Your own assemblies import like extension modules. Put Acme.Pricing.dll in a folder on sys.path - next to your script, for example - and import its namespace:

from Acme.Pricing import PriceList           # finds Acme.Pricing.dll on sys.path and loads it

For an assembly in a folder you would rather not add to sys.path, load it by its path with the clr module:

import clr
clr.AddReference(r"C:\Libraries\Acme.Pricing.dll")
from Acme.Pricing import PriceList

clr also holds a few helpers, used below: clr.python_enum, clr.cancellable, clr.GetClrType and clr.reflection_enabled. Code written for pythonnet, which starts with import clr and clr.AddReference(...), runs unchanged.

Working with .NET objects

Create objects by calling the class, and use their methods, properties, fields and indexers as you would in C#:

from System.Text import StringBuilder

sb = StringBuilder()
sb.Append("a").Append(1).Append(2.5)
print(sb.ToString())          # a12.5
print(sb.Length)              # 5

Overloads are chosen from the types of the Python arguments, and keyword arguments and default parameter values work. .NET objects also fit Python's protocols: an IEnumerable is iterable, an IDisposable works in a with statement, collections support len(), in and indexing (negative indices included), and operators defined in C# work in Python.

from System.IO import StringWriter

with StringWriter() as w:     # Dispose() is called at the end of the block
    w.Write("written by .NET")
    print(w.ToString())

Generics

Give type arguments in square brackets:

names = List[str]()
names.Add("Ada")
names.Add("Grace")
print(len(names), names[0], list(names))   # 2 Ada ['Ada', 'Grace']

ages = Dictionary[str, int]({"Ada": 36})    # built from a Python dict
ages["Grace"] = 85
print("Ada" in ages)                         # True

Generic methods infer their type arguments from the arguments when they can; otherwise give them explicitly, as in Enumerable.Where[int](numbers, predicate).

out and ref parameters

They come back as extra return values, in a tuple:

from System import Int32

ok, value = Int32.TryParse("42")        # True 42
ok, value = Int32.TryParse("forty")     # False 0

Delegates and events

A Python function (or lambda) can be passed wherever .NET expects a delegate, and subscribed to events with += (and removed with -=):

from System.Linq import Enumerable
evens = Enumerable.Where[int](List[int]([1, 2, 3, 4]), lambda n: n % 2 == 0)
print(list(evens))                       # [2, 4]

from System.Collections.ObjectModel import ObservableCollection
items = ObservableCollection[str]()
def changed(sender, e):
    print("collection changed:", e.Action, list(e.NewItems))
items.CollectionChanged += changed
items.Add("bolts")                       # collection changed: Add ['bolts']

Enums

.NET enum values behave like integers and combine with |, & and ~:

from System.IO import FileAccess
flags = FileAccess.Read | FileAccess.Write
print(flags, flags == FileAccess.ReadWrite, int(flags))   # ReadWrite True 3

clr.python_enum(FileAccess) gives a Python enum.IntFlag (or IntEnum) with the same members, for code that expects Python enums.

Implementing .NET interfaces and deriving from .NET classes

A Python class can implement a .NET interface or derive from a .NET class, and .NET code then calls its methods:

from System.Collections.Generic import IComparer

class Descending(IComparer[int]):
    def Compare(self, x, y):
        return y - x

numbers = List[int]([3, 1, 2])
numbers.Sort(Descending())       # .NET's sort calls the Python Compare
print(list(numbers))             # [3, 2, 1]

Overriding a virtual method of a .NET base class works the same way, and super() reaches the .NET implementation.

Async: awaiting .NET tasks

.NET Task and ValueTask objects can be awaited in Python coroutines, and IAsyncEnumerable objects iterated with async for:

import asyncio
from System.Threading.Tasks import Task

async def main():
    await Task.Delay(10)                 # a .NET task, awaited by asyncio
    return "awaited a .NET task"

print(asyncio.run(main()))

asyncio.gather and asyncio.wait_for work with them. To let asyncio cancel a .NET operation, pass it a cancellation token with clr.cancellable: await clr.cancellable(lambda token: client.DownloadAsync(url, token)) cancels the token when the asyncio task is cancelled.

How values convert

Values cross between Python and .NET in the natural way, without copying where the layout allows:

Python .NET
None null
bool, int, float bool, the integer types (OverflowError when out of range), double, float
decimal.Decimal decimal, exactly
str string, char
datetime, date, time, timedelta DateTime and DateTimeOffset, DateOnly, TimeOnly, TimeSpan
uuid.UUID, pathlib.Path Guid, FileInfo and DirectoryInfo
bytes, bytearray, numpy arrays byte[] and other arrays, Memory<T> and ReadOnlyMemory<T> (shared, not copied, where possible)
list, tuple, set, dict arrays, List<T>, HashSet<T>, Dictionary<K,V> and their interfaces
functions delegates
coroutines, async generators Task<T>, IAsyncEnumerable<T>

Results coming back from .NET become the corresponding Python values - which means they are Python objects afterwards: DateTime(2026, 10, 29) gives a Python datetime, with Python's methods (+ timedelta(days=3)), not .NET's (AddDays).

.NET arrays of numbers share their memory with Python's buffer protocol, so numpy works on them in place:

from System import Array
import numpy as np

a = Array[float]([1.0, 2.0, 3.0])
np.asarray(a)[:] *= 10          # changes the .NET array itself
print(list(a))                  # [10.0, 20.0, 30.0]

Exceptions

A .NET exception arrives in Python as an exception that is both a .NET type and the matching built-in Python exception, so either except clause catches it:

try:
    Int32.Parse("x")
except ValueError as e:          # also catchable as System.FormatException
    print(type(e).__name__)      # FormatException

ArgumentException and FormatException are ValueErrors, KeyNotFoundException a KeyError, IndexOutOfRangeException an IndexError, InvalidOperationException a RuntimeError, IOException an OSError (FileNotFoundException a FileNotFoundError), NotImplementedException a NotImplementedError, and so on. The exception keeps its .NET members: e.Message, e.StackTrace, e.InnerException.

Your own C# code

Any .NET class library can be used from Python: put its .dll on sys.path and import its namespace. To write a library for Python - with its own module name, native-speed bindings, and classes Python can subclass - see Writing extensions. To go the other way and run Python inside a C# application, see Embedding in C#.

Native AOT builds

Everything above uses .NET reflection, which a natively compiled (Native AOT) build of SteelPython leaves out. In such a build, import clr; clr.reflection_enabled is False, and only the .NET types its host application exposed are importable. Check clr.reflection_enabled when you are unsure which build you have; in a regular .NET build it is True and everything on this page works.