Skip to content
Pavel Koneski edited this page Apr 5, 2025 · 1 revision

One of the main purposes of IronPython is as a .NET programming language. This article briefly describes a few, perhaps not so obvious cases where IronPython is used to access the .NET library (or any other managed code). For more in-depth coverage of programming .NET with IronPython, refer to an excellent book "IronPython in Action" by Michael J. Foord and Christian Muirhead (ISBN 9781933988337). That book covers IronPython 2, but is still largely accurate for IronPython 3. For main differences between IronPython 3 and 2, refer to Upgrading from IronPython2.

Builtin Types

Some Python types are implemented using corresponding .NET types. For instance bool is implemented with System.Boolean. By default, only Python-specific members are exposed. However, importing module System brings all .NET members into the scope.

>>> dir(bool)
['__abs__', '__add__', '__and__', '__bool__', '__class__', '__delattr__', '__divmod__', '__doc__', '__eq__', '__float__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__int__', '__invert__', '__le__', '__long__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
>>> import System
>>> dir(bool)    
['Abs', 'Add', 'AsDecimal', 'AsInt32', 'AsInt64', 'AsUInt32', 'AsUInt64', 'BitwiseAnd', 'BitwiseOr', 'Compare', 'CompareTo', 'Create', 'DivRem', 'Divide', 'Equals', 'ExclusiveOr', 'FalseString', 'GetBitCount', 'GetByteCount', 'GetHashCode', 'GetType', 'GetTypeCode', 'GetWord', 'GetWordCount', 'GetWords', 'GreatestCommonDivisor', 'IsEven', 'IsNegative', 'IsOne', 'IsPositive', 'IsPowerOfTwo', 'IsZero', 'LeftShift', 'Log', 'Log10', 'Max', 'MemberwiseClone', 'Min', 'MinusOne', 'Mod', 'ModPow', 'Multiply', 'Negate', 'One', 'OnesComplement', 'Parse', 'Pow', 'ReferenceEquals', 'Remainder', 'RightShift', 'Sign', 'Square', 'Subtract', 'ToBigInteger', 'ToBoolean', 'ToByte', 'ToByteArray', 'ToChar', 'ToDecimal', 'ToDouble', 'ToFloat', 'ToInt16', 'ToInt32', 'ToInt64', 'ToSByte', 'ToSingle', 'ToString', 'ToType', 'ToUInt16', 'ToUInt32', 'ToUInt64', 'TrueString', 'TryParse', 'Xor', 'Zero', '__abs__', '__add__', '__and__', '__bool__', '__class__', '__complex__', '__delattr__', '__divmod__', '__doc__', '__eq__', '__float__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__int__', '__invert__', '__le__', '__long__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
>>> bool is System.Boolean
True

int Type

The Python int type in IronPython 3 is implemented with System.Numerics.BigInteger (and not as System.Int32 as it was in IronPython 2). It can contain in theory an arbitrarily large integer (only limited by the 2 GByte memory boundary).

>>> import clr
>>> clr.AddReference("System.Numerics")
>>> import System
>>> int is System.Numerics.BigInteger
True
>>> int is System.Int32
False
>>> clr.GetClrType(int).Name
'BigInteger'

However, mostly for performance reasons, IronPython will use instances of System.Int32 to hold smaller integers. This is largely transparent from the Python side, but the distinction may become relevant for interop cases. Examples:

i = 1        # instance of Int32
j = 1 << 31  # instance of BigInteger
k = j - 1    # still BigInteger, as one of the arguments makes the result type BigInteger

This means that the type of Int32 objects is reported as int (which is the same as BigInteger). If it is important to check what is the actual type of a given integer object, test for the presence of MaxValue or MinValue. For those properties to be visible, System has to be imported first.

>>> import System
>>> type(i)
<class 'int'>
>>> hasattr(i, 'MaxValue') # Int32
True
>>> hex(i.MaxValue)
'0x7fffffff'
>>> type(j)
<class 'int'>
>>> hasattr(j, 'MaxValue') # BigInteger
False

The creation of either Int32 or BigInteger instances happens automatically by the int constructor. If for interop purposes it is important to create a BigInteger (despite the value fitting in 32 bits), use method ToBigInteger. It converts Int32 values to BigInteger and leaves BigInteger values unaffected.

>>> bi = i.ToBigInteger()
>>> hasattr(bi, 'MaxValue')
False

In the opposite direction, if it is essential to create Int32 objects, either use constructors for int or Int32. The former converts an integer to Int32 if the value fits in 32 bits, otherwise it leaves it as BigInteger. The latter throws an exception is the conversion is not possible.

For more examples see Upgrading from Python2 — int Type .

Generic Types

Specialized types based on generic types in .NET can be constructed using the indexed-type notation. The indexer is the type to be used in place of a generic type.

Example.

>>> from System.Collections.Generic import List
>>> python_list = list(range(10))
>>> cli_list = List[int](python_list)
>>> cli_list
List[int]([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> cli_list.Add(10)
>>> list(cli_list) 
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

CLR Arrays

CLR arrays are usually created using the generic class construction syntax. Example:

from System import Array, Int32
arr1 = Array[string](10)
arr2 = Array[Int32](2, 3)

The above is equivalent to the following C# code:

string[] arr1 = new string[10];
int[,] arr2 = new int[2, 3];

One-dimensional arrays can be created and initialized in one step, by providing an iterable object with a defined length to the constructor:

>>> vec = Array[Int32](range(10))
>>> vec
Array[Int32]((0, 1, 2, 3, 4, 5, 6, 7, 8, 9))

A less common way of creating arrays is by using static method CreateInstance. This allows for greater flexibility in some corner cases. For instance, let's create a 1-based 2x2 matrix (2-dimensional array):

matrix = Array.CreateInstance(str, (2,2), (1,1))
matrix[1, 1] = "x_1,1"
matrix[1, 2] = "x_1,2"
matrix[2, 1] = "x_2,1"
matrix[2, 2] = "x_2,2"

Nonzero-based arrays are fully supported, including negative indices counting from the end.

>>> matrix[1, 1] == matrix[-2, -2]
True

This is the Pythonic way of interpreting negative indices as relative from the end, but it breaks down if the array base is negative too. In such case negative indices are treated as absolute, to allow access to the part of the array that uses negative indices. For instance, consider a 3-element array indexed from -1 to 1.

>>> symmetricArr = Array.CreateInstance(int, (3,), (-1,))
>>> symmetricArr
Array[int]((0, 0, 0), base=-1)
>>> symmetricArr[-1] = 10
>>> symmetricArr[1] = -10 
>>> symmetricArr
Array[int]((10, 0, -10), base=-1)

Summary of indexing of CLR arrays in IronPython:

Base Index >= 0 Index < 0
> 0 absolute relative from end
0 absolute == relative from beginning relative from end
< 0 absolute absolute

Comparison to indexing in C# and CPython:

  • Index >= 0, any base is C# compliant.
  • Base 0, any index is CPython compliant.
  • Base 0, index < 0 is not supported by C# but can be achieved by System.Index with 1-dim arrays only; then IronPython indexing is C# compliant (as well as CPython compliant) in principle (support for System.Index is not implemented).
  • Base > 0, index < 0 is not supported by C#; IronPython follows the CPython convention as more practical.
  • Base < 0, index < 0 is C# compliant.
  • Base != 0 is not supported by CPython for any builtin structures.

Summary of indexing of CLR arrays in C#:

Base Index >= 0 Index < 0
> 0 absolute N/A
0 absolute N/A
< 0 absolute absolute

Summary of indexing of CLR arrays in CPython:

Base Index >= 0 Index < 0
> 0 N/A N/A
0 relative from beginning relative from end
< 0 N/A N/A

The table for IronPython is basically a merge of the tables for C# and CPython.

For more detailed explanation of indexing see https://github.com/IronLanguages/ironpython3/pull/1828#discussion_r1870572832

Out and Ref Parameters

To call methods that use an out or ref parameter, a strongly-typed box can be created and provided as the argument.

>>> import clr, System
>>> box = clr.StrongBox[float]()  # initialized to 0.0
>>> # Calling public static bool TryParse(string? value, out double result);
>>> System.Double.TryParse("3.1416", box)
True
>>> box.Value
3.1415999999999999

The input value for the box used as a ref argument can be provided as argument to the box constructor or by assigning its property Value. An alternative to clr.StrongBox is clr.Reference, which is just a forward to StrongBox.

For out arguments, a simplified call convention is supported, where the out argument is omitted and instead a tuple is returned containing the actual return value as the first element (if the method return type is not void), with subsequent elements being values for the missing out arguments. If the tuple would be of length 1, the single element is returned instead.

>>> import System
>>> # Calling public static bool TryParse(string? value, out double result);
>>> System.Double.TryParse("3.1416")
(True, 3.1415999999999999)

For ref arguments a similar convention holds, except that the arguments cannot be omitted. The output values for those arguments are still included in the return.

>>> import System
>>> arr = System.Array[bool](4)
>>> arr
Array[bool]((False, False, False, False))
>>> System.Array.Resize(arr, 6)
Array[bool]((False, False, False, False, False, False))

Extension Methods (e.g. LINQ)

Extension methods are by default not visible and must be explicitly imported before used. Also the DLL containing the extensions has to be referenced first.

import clr, System
clr.AddReference("System.Core")
clr.ImportExtensions(System.Linq)
query = range(10).Where(lambda x: x % 2 == 0).Select(lambda x: 2 ** x)
list(query)  # executes query, result: [1, 4, 16, 64, 256]

Intefaces

.NET interface types are modelled as abstract Python classes. This means it is possible to make an unbounded method call on an interface-derived class by providing self as the extra first argument.

For instance, the following C# expressions (using redundant casts for clarity)

((System.IComparable<int>)1).CompareTo(2)
((System.IComparable<System.Numerics.BigInteger>)new System.Numerics.BigInteger(1)).CompareTo(2)

have equivalent IronPython expressions

System.IComparable[System.Int32].CompareTo(1, 2)
System.IComparable[int].CompareTo(1, 2)

Span and Memory

.NET Span<T> and ReadOnlySpan<T> type are ref-structs and instances of such types cannot be boxed. Therefore it is not possible to have a Python variable referring to an instance of Span<T> or ReadOnlySpan<T>. Moreover, it is not possible to make calls from IronPython to methods that take spans as arguments (since existing reflection invoke APIs require boxing).

On the other hand, Memory<T> and ReadOnlyMemory<T> is fully supported.

import System
arr = System.Array[System.Byte](10)
dest = System.Memory[System.Byte](arr)

For more info why spans are not supported, see the docs.

There is a proposal to implement this support in .NET

Casting

Python does not have a special casting syntax like C# or C++. It is usually not necessary in a dynamic language and if it is, usually the type constructor is sufficient, e.g. float(i) converts an object referred by i into float (if possible). Type constructors in IronPython do that as well, including for CLR types. For convenience, those constructors are often augmented to accept arguments beyond what the CLR defines. For instance, System.UInt64(1) creates an object of type System.UInt64 and initializes it to hold value 1. Or System.Char(65) creates a System.Char object with value "A". In cases where such support is not (yet) provided, one can invoke a conversion operator (explicit or implicit) defined for the given type by using clr.Convert. Beyond engaging conversion operators, this function will also apply typical Python conversions in order to succeed.

>>> import System, clr
>>> System.Char(65)  # using constructor
A
>>> clr.Convert(65, System.Char)  # using cast, same result
A
>>> clr.Convert(1.23456789, System.Half)  # using cast
<System.Half object at 0x000000000000002B [1.234]>
>>> clr.Convert("abc", System.Boolean)  # in Python, everything is truthy or falsy
True