In our recent Dynamo 3.4 announcement we mentioned the availability of a new package, “PythonNet3”, which provides a new engine for Dynamo’s Python nodes (“Python Script” and “Python Script from String”). This post is a deep-dive into that terse announcement, exploring what the new engine does differently, how to transition, and where we go from here. We’re going into all the hairy detail here, and we’ll assume some knowledge of Dynamo and Python. If you need a refresher, the Primer is a great place to start.
The purpose of the new engine is to close the gap that has existed for some time between what we are able to do in the IronPython engines and the current state with the default CPython3 engine. This first release doesn’t yet close the gap, but it’s a step in that direction, with more to come!
Before we get started, we want to add a special thank you to Cyril Poupin (blog, forum), a long-time Dynamo expert, crack Python coder, blogger, and active community member. He has generously shared his in-depth knowledge of Python with our team throughout our work on the new engine, and has spent significant time testing the package and providing feedback. Thank you!
Edits
2025-02-03: Version 1.1.0 – simplify inheriting from .NET
Contents
The Landscape
We’ll start with an overview of the context in which this new package sits, as this informs subsequent discussion of new features and caveats. If you’re already familiar with IronPython, Python.NET, and the current landscape of Dynamo Python engines, feel free to skip to What’s New!
IronPython vs Python.NET
How hard can it be? You just run a Python script and return the result! And yes, if all we wanted to do was send text and numbers back and forth from Dynamo to our Python scripts, we wouldn’t need all this fuss. But we want to do a lot more than that. We want to send complex objects and geometry back and forth and we want to use Python to define logic for Dynamo’s hosts to use, for example to define filtering logic for a Revit element collector. This is hard to support because Dynamo and its hosts exist in a separate world from the world Python code typically exists in. Dynamo runs inside of the .NET runtime environment, while Python is typically interpreted and executed by CPython, the reference implementation of the Python language, written in C. This means Dynamo and Python have very different ways of representing objects and functions and, except in very simple cases, converting between the two is hard. There are two main solutions out there to deal with this gap:
IronPython: There is no gap
If Python code ran in the same place as Dynamo code, there wouldn’t be any gap to worry about. This is the approach taken by IronPython, an open-source library originally developed by Microsoft. It (re)implements the whole Python language in C# (a .NET language) so that our Python scripts are interpreted and executed in the same place Dynamo, Revit, etc. are executing. This makes it very smooth for our Python code to talk with Dynamo and its hosts, and is one reason why support for very .NET/Windows-y things like WPF and COM is so good in IronPython. However, there are some drawbacks:
- IronPython reimplements CPython and thus needs to redo all the work CPython did. Sixteen years after the first release of Python 3, they still haven’t finished their implementation. (A comment on the size of the task, not on the valiant efforts of the open-source community!)
- Since CPython is written in C, and C typically runs faster than Python, many Python libraries are written in C or C++. These libraries don’t work with IronPython. This includes popular libraries such as numpy and pandas.
Python.NET: Bridging the gap
Instead of reimplementing CPython, Python.NET, another open-source library, builds a bridge between the worlds of .NET and CPython. Python.NET consists of a .NET library for communicating with an instance of CPython and a Python library for communicating back with the .NET environment. There shouldn’t be any question of Python scripts or libraries not working in Dynamo-Python.NET land, since they’re running in CPython. But communication with .NET is harder because Python.NET has to do the work of translating objects and methods between the two environments.
While it’s not perfect, we believe Python.NET is the better path going forward. It’s a thinner slice of customization to link .NET and Python, and allows us to immediately benefit from work done in the massive ecosystem that is Python and its world of packages.
Dynamo Python Packages
Armed with the context of what IronPython and Python.NET are, let’s take a look at the various Python engines in Dynamo. We have (now) four different Python engines (that we know of!):
IronPython2
Built into Dynamo pre-2.8. Now a package on the package manager.
The OG! Life was pretty good back in the days of Python 2. Unfortunately, those days are long gone. Python 2 hasn’t received security updates in 15 years, which is kind of a long time for interested folks to find ways to mess with it.
IronPython3
Package on the package manager.
The logical next step, in many ways. But the IronPython 3 extension is unofficial because of the drawbacks to IronPython above.
CPython3
Built into Dynamo starting in 2.7. Default Python engine starting in 2.8.
Instead, our next engine, CPython3, switched to use Python.NET. The CPython3 engine uses Python.NET version 2.5. Not to be confused with the very old Python 2.5 – Python.NET 2.5 supports Python 3 – but it does have some impactful limitations around .NET interfaces, WPF, COM, and others.
PythonNet3
A hero arises! PythonNet3 is based, as might be guessed, on Python.NET version 3 (a major version change because of breaking changes in Python.NET itself, not because of changes in Python). Version 3 of the Python.NET library brings many improvements and optimizations that we’ve been looking forward to making use of for a while now.
Why a New Engine?

One engine to deprecate them all! But seriously, here are the reasons why we need a new engine:
Package all the things
Extracting core functionality into packages brings several advantages. One of the biggest is that they’re updateable and removable independent of Dynamo. Now that we have Python in a package, we can deliver updates as often as we have them and many users can get those updates immediately without needing to wait for the next release of Revit/Civil 3D/etc. This also means that users of current versions of Dynamo can stay up-to-date with the latest Python functionality even if they aren’t able to update to new versions of Dynamo. We could make a package of the CPython3 engine, but we thought that would be too confusing in versions of Dynamo where CPython3 is already built-in.
Preserve existing behavior
Making a new engine, in a separate package, also allows us to leave the current CPython engine undisturbed. PythonNet3 is the future for Python in Dynamo, but we want to be certain we get that future right before we make it the default experience. We hope that separate engines will make it more clear to graph authors and users when they are using “the new thing”. Separate engines also allow side-by-side comparison of the engines in the same script.
What’s New in PythonNet3
Nice story, but what can I do with this new thing? Yep yep, here it is! The general theme, with some caveats that we’ll get into, is that CPython3 scripts should work the same in PythonNet3. There are new things, and simpler ways of doing things, in PythonNet3, but the old ways still work. In some special cases, though, behavior will change or break. We’ll highlight those as we go. Here’s a summary of the changes:
Breaking changes
- Updated packages: check for use of packages which have a major update.
- Call base constructor: look for
def __init__in classes that inherit from a .NET type and addsuper().__init__(). - Enum conversion: look for uses of .NET enums (like
BuiltInCategory). - Some open bugs
Opportunities
- Use LINQ!
- Implement
outandrefmethods: .NET interfaces usingoutandrefare fair game. - Better overload support: look for references to
System.Reflection– you might not need it. - Use overloaded C# operators: Add, subtract, etc. .NET types as designed.
- Iterable detection:
hasattr(x, '__iter__')works as expected.
New Package Versions
One potentially breaking change is that we’re updating the versions of the Python packages that ship with the engine. In some cases we were shipping pretty old package versions with CPython3, so this will give you access to the latest and greatest out-of-the-box. But depending on what functionality you rely on, you may need to rewrite parts of your scripts to adapt to these new versions. As with CPython3, you can install your own packages, or modify the version of these default packages, by following this documentation. Here’s a look at the version number changes:
Potentially breaking updates
- NumPy 1.24.1 -> 2.1.2 – upgrade guide
- pandas 1.5.3 -> 2.2.3 – no official guide that we found, but here’s an unofficial guide
- Pillow 9.4.0 -> 11.0.0
- SciPy 1.10.0 -> 1.14.1
Other updates
- ContourPy 1.0.7 -> 1.3.0
- cycler 0.11.0 -> 0.12.1
- et_xmlfile 1.1.0 -> 2.0.0
- fonttools 4.38.0 -> 4.54.1
- kiwisolver 1.4.4 -> 1.4.7
- matplotlib 3.6.3 -> 3.9.2
- openpyxl 3.0.10 -> 3.1.5 – removed some already deprecated methods
- Packaging 23.0 -> 24.1 – minimal breaking changes
- PyParsing 3.0.9 -> 3.2.0
- python-dateutil 2.8.2 -> 2.9.0
- pytz 2022.7.1 -> 2024.2
- six 1.16.0 – 1.16.0
New Features
Better extension method support (LINQ!)
We have extension method support in CPython3, but only for simple cases. A number of improvements on our side and in Python.NET converged, resulting in much more robust support. Most .NET extension methods should now work, including all the goodies in LINQ. Here’s an example listing the names of trees in a Revit model:
import clr
import System
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager
clr.ImportExtensions(System.Linq)
doc = DocumentManager.Instance.CurrentDBDocument
OUT = FilteredElementCollector(doc).OfClass(FamilyInstance)\
.WhereElementIsNotElementType()\
.Where(System.Func[Element, bool](lambda e: "Tree" in e.Name))\
.Select[Element, str](System.Func[Element, str](lambda e: e.Name))
Code language: Python (python)
There are a few remaining caveats which we’re looking into:
- Some methods need a little typing help. Notice that we don’t need to specify types on
Where(line 12 above), but we do onSelect(line 13). - When passing a lambda as a function parameter we always need to explicitly cast it, e.g., to
System.Func[<input type>, <output type>]. (This isn’t specific to extension methods, just comes up a lot with LINQ.) - Some extension libraries still don’t work. The only one we’re aware of is .NET’s
DataTableExtensions. We’re looking into it, but if you find others that don’t work, please let us know!
Simplified inheritance from .NET classes and interfaces
The bug encountered when defining a Python class inheriting from a .NET class or implementing a .NET interface is fixed starting in PythonNet3 version 1.1.0. Inheritance now works just as you’d expect, but see the section on overriding an out parameter for an example. (Thanks to this PR to Python.Net for the method.)
Click here for the workaround, now only needed for CPython3. Thanks to Cyril Poupin, who wrote a blog post describing this solution!
import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
clr.AddReference("RevitServices")
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
class MyFamilyLoadOptions:
def __new__(cls, *args, **kwargs):
cls.args = args
# !! Modify namespace when modifying _InnerClass !!
cls.__namespace__ = "MyFamilyLoadOptions_xpkb"
try:
# Has the class already been registered?
return __import__(cls.__namespace__)._InnerClass(*cls.args)
except ImportError as ex:
# Must be our first time, define the class
class _InnerClass(IFamilyLoadOptions):
__namespace__ = cls.__namespace__
def __init__(self):
super().__init__()
def OnFamilyFound(self, familyInUse, _overwriteParameterValues):
overwriteParameterValues = True
return (True, overwriteParameterValues)
def OnSharedFamilyFound(self, sharedFamily, familyInUse, source, _overwriteParameterValues):
overwriteParameterValues = True
return (True, overwriteParameterValues)
return _InnerClass(*cls.args)
doc = DocumentManager.Instance.CurrentDBDocument
TransactionManager.Instance.EnsureInTransaction(doc)
opts = MyFamilyLoadOptions()
loadFamily = doc.LoadFamily(IN[0], opts, None)
TransactionManager.Instance.TransactionTaskDone()
This approach creates a wrapper class which, in its constructor, defines the actual class only on first run. It then returns an instance of the actual class from __new__, essentially becoming the actual class. If we need to edit the inner class though, the wrapper won’t know to reload it. In this case we need to change the namespace of the inner class (line 12) so that the wrapper loses track of the previous version and re-registers the new inner class. Note that the namespace and class definition name is shared across all Python nodes of the same engine in the Dynamo script. To avoid gotchas, also change the namespace when duplicating a Python node!
Python.NET 3 Updates
There’s a lot in this update that we can’t take credit for! The community that builds the Python.NET library has put in a lot of work since their version 2.5 that we use in the CPython3 engine. The update to version 3 is a large breaking change. Those breaking changes are mostly behind the scenes, and shouldn’t affect most scripts, but the deeper a script gets into the Python-.NET relationship, the more likely its behavior will change when updating. We’ll highlight some notable changes and new features here. If you really want to see all the gory detail though, take a look at their changelog. Thanks to Cyril for picking out the changes that are most relevant for Python in Dynamo!
Implement or override .NET methods with an out or ref
One common desired use-case for Python in Dynamo is to define custom logic for use in the Revit or Civil 3D API, for example a custom method for filtering elements. This is typically done by implementing a .NET interface from the Revit API using a Python class, and then passing an instance of that Python class to a Revit API method call. In IronPython this worked great, but in Python.NET there have historically been a few issues. One narrow issue is that in CPython3 we can’t implement methods with `out` or `ref` parameters. In Python.NET 3, now we can! Here’s an example of defining a custom Revit family loader:
import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
clr.AddReference("RevitServices")
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
class MyFamilyLoadOptions(IFamilyLoadOptions):
__namespace__ = "ns"
def __init__(self):
super().__init__()
def OnFamilyFound(self, familyInUse, _overwriteParameterValues):
overwriteParameterValues = True
return (True, overwriteParameterValues)
def OnSharedFamilyFound(self, sharedFamily, familyInUse, source, _overwriteParameterValues):
overwriteParameterValues = True
return (True, overwriteParameterValues)
doc = DocumentManager.Instance.CurrentDBDocument
TransactionManager.Instance.EnsureInTransaction(doc)
opts = MyFamilyLoadOptions()
loadFamily = doc.LoadFamily(IN[0], opts, None)
TransactionManager.Instance.TransactionTaskDone()
Code language: Python (python)
The signature of OnFamilyFound in .NET is:
bool OnFamilyFound(bool familyInUse, out bool overwriteParameterValues)Code language: C# (cs)
In Python, to implement this method we return a tuple with the method return value as the first item and values for out or ref parameters as subsequent items, ordered as they are in the method’s signature. Note that we don’t actually need the overwriteParameterValues variable; the method could be a one-liner: return (True, True). But that’s pretty cryptic, and we’d rather be kind to those that come after us, ourselves included!
Improved support for generic method overloading
In .NET it is possible to define overloaded class methods – methods which all have the same name but different numbers or types of parameters. The compiler chooses which version of the method to use based on the parameters that have been passed. When we call an overloaded .NET method from Python, it’s up to Python.NET to handle this choosing logic, something it didn’t do so well in version 2.5 (CPython3). In version 3 it’s much better! Here’s an example showing a Revit dialog where the overloaded TaskDialog.Show method works as expected:
import clr
clr.AddReference('RevitAPIUI')
from Autodesk.Revit.UI import (TaskDialog, TaskDialogCommonButtons)
dialog = TaskDialog('Well Hello There')
dialog.CommonButtons = TaskDialogCommonButtons.Ok | TaskDialogCommonButtons.Yes
OUT = dialog.Show()Code language: Python (python)

We can use overloaded methods in CPython3 by using .NET reflection to manually pick the correct method. This still works in PythonNet3, so this isn’t a breaking new feature, but any time you see System.Reflection in your CPython3 scripts you might be able to remove it in PythonNet3.
Use overloaded C# operators
In C#, classes and structs can overload binary and unary operators. An example from the Revit API is the XYZ 3D point:
new XYZ(10, 10, 0) + new XYZ(1, 1, 1)Code language: C# (cs)
.NET knows how to add the two points because the XYZ class defines the + operator (among others). With CPython3 it wasn’t possible to use these overloads from Python. In Python.NET 3 support for this was added! The following now works as expected in Dynamo with PythonNet3:
import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import XYZ
OUT = XYZ(10, 10, 0) + XYZ(1, 1, 1)Code language: Python (python)
Breaking: overriding .NET class’s constructor
When writing a Python class which inherits from a .NET class and overriding the constructor (__init__), Python.NET 3 no longer calls the base class’s __init__ for us. This gives us more flexibility on exactly when we call the base constructor, but when converting from CPython3 we will need to add this in with super().__init__().
from System.Collections.Generic import List
class MyList(List[str]):
def __init__(self):
super().__init__() # <- Add this in PythonNet3
self.AddRange(["a", "b"])
l = MyList()
OUT = l[0]
Code language: Python (python)
Breaking: explicit enum conversion
In CPython3 enumeration objects from .NET are automatically converted to their integer equivalent in Python. This is occasionally convenient, but makes some things harder, or impossible. Now .NET enums remain .NET enums, and if we want to get their integer value we can do so explicitly, just like in .NET.
import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import BuiltInCategory
c = BuiltInCategory.OST_BridgeTowers
c2 = BuiltInCategory(-2006132)
OUT = (c, int(c), c2)Code language: Python (python)

Proper iterable detection
Python.NET 3 brings a small but impactful bug fix regarding iterables. In Python.NET 2.5 (as seen in the CPython3 engine), all .NET classes are considered iterable. This makes it harder to figure out whether a .NET object is a list or a single value. One common scenario where this has been annoying is when operating on input data from a host app. When working with selections or filters, often if there’s only a single piece of data it comes in as a single object, and if there are multiple pieces of data they come in as a list. This is nice when we know we’re dealing with just a single item, but if we want to handle both cases we probably want to convert that single item into a list of length one. Easy enough in Python, right?
toList = lambda x : x if hasattr(x, '__iter__') else [x]
inputList = toList(IN[0])Code language: Python (python)
But due to the bug mentioned above, in CPython3 that hasattr(x, '__iter__') check, testing whether x is a list (iterable), always returns true. In Python.NET 3 now it doesn’t! Here’s the difference in Dynamo on the selection of a family instance. The CPython3 version doesn’t create an enclosing list, while the PythonNet3 version does.

Migrating
Migrating from CPython3 to PythonNet3
First of all, migrating scripts from CPython3 to PythonNet3 is not a requirement. CPython3 will remain the default Python engine in Dynamo for at least the rest of 2025. But if you want to take advantage of the new goodies or be proactive, here are some thoughts and tips.
- Take advantage of the new (in the Dynamo 3.3 release) bulk Python engine switcher (
Edit > Set Python Engine) and see what happens! - CPython3 and PythonNet3 can coexist in separate nodes within the same graph. Copy-paste a CPython3 node, switch to PythonNet3, and compare the behavior side-by-side.
Migrating from IronPython to PythonNet3
If you don’t have a specific reason to be using IronPython in Dynamo, try switching to PythonNet3 – you might like it! If you do have a specific reason though, you might still want to stay for a bit. Here are the reasons we know of that people use IronPython instead of CPython3 (essentially a summary of the to-dos in the What’s Next section!):
- .NET Interface implementation – Fixed in PythonNet3 1.1.0!
- WPF – It’s not actually that bad. See below for some tips.
- COM – Yep, stay where you are! We’ll be thinking more about COM soon.
If you do decide to make the move, see the previous section on migrating from CPython3 for some tips.
What’s Next for the Team
We’re not done yet! Our goal is to make PythonNet3 comparable to IronPython in features and ease of use. Here’s what’s on our radar going forward.
WPF
Using WPF with an MVVM pattern to display a custom window is possible in PythonNet3, and just a bit nicer than in CPython3, but not much has changed so it currently takes more effort compared to IronPython. Here’s an example with the best approach we know of at the moment, thanks to (you guessed it!) Cyril:
import clr
import System
clr.AddReference("System.Xml")
clr.AddReference("PresentationFramework")
clr.AddReference("PresentationCore")
clr.AddReference("System.Windows")
clr.AddReference("DynamoCore")
from System.Windows import LogicalTreeHelper
from Dynamo.Core import NotificationObject
class ViewModel:
def __new__(cls, *args):
cls.args = args
# !! Modify namespace when modifying _ViewModel !!
cls.__namespace__ = "ViewModel_zlcg"
try:
return __import__(cls.__namespace__)._ViewModel(*cls.args)
except ImportError:
class _ViewModel(NotificationObject):
__namespace__ = cls.__namespace__
def __init__(self):
super().__init__()
_text = "Hieee"
def get_Text(self):
return self._text
def set_Text(self, value):
self._text = value
self.RaisePropertyChanged("Text")
Text = clr.clrproperty(str, get_Text, set_Text)
return _ViewModel(*cls.args)
class MyWindow(System.Windows.Window):
xaml = '''
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="82" Width="350">
<StackPanel Orientation="Horizontal" Margin="10">
<Label Content="Text:"/>
<Label Content="{Binding Text}"/>
<TextBox Margin="10 0" Width="150"
Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}" />
<Button x:Name="Button" Content="Reverse" />
</StackPanel>
</Window>'''
def __new__(cls):
reader = System.Xml.XmlReader.Create(System.IO.StringReader(cls.xaml))
window = System.Windows.Markup.XamlReader.Load(reader)
window.__class__ = cls
return window
def __init__(self):
super().__init__()
self.DataContext = ViewModel()
LogicalTreeHelper.FindLogicalNode(self, "Button").Click += self.buttonClick
def buttonClick(self, sender, e):
self.DataContext.Text = self.DataContext.Text[::-1]
def showWindow():
try:
window = MyWindow()
window.Title = "Test"
window.ShowDialog()
except Exception as ex:
print(ex)
System.Windows.Application.Current.Dispatcher.Invoke(System.Action(showWindow))
Code language: Python (python)
Some notes:
- We’re using Dynamo’s
NotificationObjectwhich is a basic implementation ofINotifyPropertyChanged(line 20). - Declare properties by defining a private
_propNamefield andget_PropNameandset_PropNamegetter and setter methods, callingRaisePropertyChangedin the setter, and then registering everything for .NET withPropName = clr.clrproperty(str, get_PropName, set_PropName)(line 31). - We can’t bind events to elements directly in XAML. Instead, set the
x:Nameproperty on the element and useLogicalTreeHelper.FindLogicalNode(line 59). - The funny business in
__new__on line 53 merges theWindowobject we get from parsing the XAML with ourMyWindowclass. This allows for line 67, where we set a property of theSystem.Windows.Windowclass directly on ourMyWindow! Note that if our XAML defined something other than a Window, we’d need to modify the class we inherit from on line 35. - We create the
showWindowmethod and run it on the dispatcher (line 72) to ensure we’re running on Dynamo’s UI thread. This is only necessary in Sandbox: When inside Revit or Civil 3D we already end up on the right thread so we could just run line 66-68 directly.
Interaction with COM APIs
PythonNet3 doesn’t contain any change from CPython3 regarding interaction with Windows’ COM APIs. COM interaction is pretty seamless in IronPython but currently requires some effort in CPython3/PythonNet3. It’s on our radar! If you have a use-case for COM APIs in Dynamo Python, we’d love to hear about it on the forum.
Known Issues
This section in particular will likely stay in flux as we (and you!) discover, and we fix, discovered bugs. Please let us know if you find something goofy so we can add it here and look into it.
DataTableExtensions
As noted above in the section on extension methods, the .NET DataTableExtensions library doesn’t work with PythonNet3. It’s the only one we know of, but it’s probably not the only one which doesn’t work, so let us know if you find issues with another!
DSCore.List.Flatten
Here’s a cool thing you can do with CPython3:
import clr
clr.AddReference('DSCoreNodes')
import DSCore
flattened = DSCore.List.Flatten(IN[0])Code language: Python (python)
Unfortunately Flatten doesn’t do any flattening in PythonNet3. It’s on the list!
Why “PythonNet3”?
We’ll end with a little addendum of sorts on the hardest problem of all, naming. What should we call the new engine? Facts/criteria:
- Also based on CPython, but using Python 3.11 instead of 3.9
- Also based on Python.NET, but using Python.NET 3 instead of 2.5
- Intended to become the new standard
- Don’t want to keep making new engines every time we have a small bump in Python or Python.NET versions
- Keep it simple
We went around on this longer than we’d care to admit. We’re not particularly happy with the result, but it’s the best we’ve got. In the end, the term is somewhat arbitrary, and perhaps the most important aspect is that it is easy to say, write, and distinguish from the other engines. Or at least that’s what we’re telling ourselves. This is likely the biggest reason we hope this is the last Python engine – if not, we’d have to come up with another name! If you come up with something better, we’d love to know about it, but we probably won’t change anything since that would just create more confusion.
That’s all you need to know! Probably. We plan to keep this up-to-date as we learn more and continue to work on the engine. We’d love to hear your feedback, questions, or suggestions on the forum. Onward!
The Dynamo Team