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.