Description
Environment:
- Python: 3.12.x
- pythonnet: 3.0.3, 3.0.5 (and other 3.0.x versions)
- clr-loader: 0.2.6 (also tested with 0.2.7.post0)
- .NET Runtimes Tested:
- .NET 8 (e.g., 8.0.16) - Fails
- .NET 9 (e.g., 9.0.5) - Works
- OS: Windows 10/11 (primarily tested on Windows)
Problem:
When using pythonnet
3.0.x with Python 3.12 to interact with .NET 8 assemblies via CoreCLR, pythonnet
successfully loads the .NET 8 runtime and the target assemblies into the AppDomain. However, it fails to properly expose the .NET types to the Python environment. This results in AttributeError
when trying to access types directly from an imported namespace, or ImportError
when a Python module within a package attempts to import these .NET types.
The same operations succeed when the .NET assembly is compiled for .NET 9 and the .NET 9 runtime is used with the same pythonnet
version.
Observed Behavior (.NET 8):
- .NET 8 CoreCLR is loaded (confirmed via
System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription
and assemblies likeSystem.Private.CoreLib, Version=8.0.0.0
in AppDomain). - Target .NET 8 assembly is loaded into
System.AppDomain.CurrentDomain.GetAssemblies()
. clr.AddReference("Path/To/Assembly.dll")
completes without error.- Importing the assembly's namespace (e.g.,
import MyNamespace
) succeeds. - Attempting to access a type (e.g.,
MyNamespace.MyClass
) results inAttributeError: module 'MyNamespace' has no attribute 'MyClass'
. - Alternatively, if a package's Python module tries
from MyNamespace import MyClass
(afterclr.AddReference
in__init__.py
), it fails withImportError: cannot import name 'MyClass' from 'MyNamespace'
. - The
clr
module object consistently lacks theReferences
attribute (i.e.,hasattr(clr, "References")
isFalse
) after .NET CoreCLR initialization. clr.FindAssembly("AssemblyName")
returns a string path to the DLL, not anAssembly
object.
Expected Behavior:
- Types from .NET 8 assemblies should be accessible in Python after
clr.AddReference()
and importing the namespace. - No
AttributeError
orImportError
should occur when accessing correctly referenced .NET types. - The behavior of
clr.References
andclr.FindAssembly()
with .NET CoreCLR should ideally be consistent with .NET Framework or clearly documented if different.
Steps to Reproduce (Minimal Case):
-
Create a simple .NET 8 Class Library (
MinimalNet8Lib
):MinimalNet8Lib.csproj
:<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles> </PropertyGroup> </Project>
Class1.cs
(or e.g.,Greeter.cs
):namespace MinimalNet8Lib; public class Greeter { public static string SayHello(string name) { return $"Hello, {name} from .NET 8!"; } public string InstanceHello(string name) { return $"Instance hello to {name} from .NET 8!"; } }
- Build the library:
dotnet build -c Release
-
Create a Python Test Script (
test_minimal_net8.py
):import os import sys from pathlib import Path print(f"Python version: {sys.version}") print(f"Python executable: {sys.executable}") # --- Crucial: Set PYTHONNET_RUNTIME to coreclr BEFORE pythonnet import --- os.environ["PYTHONNET_RUNTIME"] = "coreclr" try: print("Attempting to import pythonnet and clr_loader...") import pythonnet import clr_loader print(f"pythonnet version: {pythonnet.__version__}") print(f"clr_loader version: {clr_loader.__version__}") except Exception as e: print(f"Error importing pythonnet/clr_loader: {e}") sys.exit(1) # --- Setup Paths --- # Adjust this path to your MinimalNet8Lib.dll # Assumes the script is run from a directory where MinimalNet8Lib/bin/Release/net8.0/MinimalNet8Lib.dll exists script_dir = Path(__file__).parent.resolve() dll_path = script_dir / "MinimalNet8Lib" / "bin" / "Release" / "net8.0" / "MinimalNet8Lib.dll" runtime_config_path = script_dir / "MinimalNet8Lib" / "bin" / "Release" / "net8.0" / "MinimalNet8Lib.runtimeconfig.json" # Optional: Specify DOTNET_ROOT if not discoverable # dotnet_root = Path(os.environ.get("DOTNET_ROOT", "C:/Program Files/dotnet")) print(f"Target DLL: {dll_path}") print(f"Runtime Config: {runtime_config_path}") if not dll_path.exists(): print(f"ERROR: DLL not found at {dll_path}") sys.exit(1) if not runtime_config_path.exists(): print(f"ERROR: Runtime config not found at {runtime_config_path}") sys.exit(1) try: print("\n--- Initializing .NET CoreCLR Runtime ---") # Using clr_loader to get the runtime rt = clr_loader.get_coreclr(runtime_config=str(runtime_config_path)) #, dotnet_root=str(dotnet_root) print(f"CoreCLR runtime object: {rt}") print("Setting pythonnet runtime...") pythonnet.set_runtime(rt) print("Pythonnet runtime set.") print("Attempting to import clr...") import clr print("Successfully imported clr.") print(f"clr module: {clr}") if hasattr(clr, "__version__"): print(f"clr (pythonnet) version attribute: {clr.__version__}") except Exception as e: print(f"ERROR during runtime initialization or clr import: {e}") import traceback traceback.print_exc() sys.exit(1) print("\n--- Runtime and Assembly Diagnostics ---") try: from System import Environment, AppDomain from System.Runtime.InteropServices import RuntimeInformation print(f"PythonEngine.Version: {pythonnet.get_version()}") print(f"PythonEngine.IsInitialized: {pythonnet.is_initialized()}") print(f"System.Environment.Version: {Environment.Version}") print(f"RuntimeInformation.FrameworkDescription: {RuntimeInformation.FrameworkDescription}") print(f"Adding reference to: {str(dll_path)}") clr.AddReference(str(dll_path)) print("clr.AddReference completed.") print("Assemblies in Current AppDomain:") for assembly in AppDomain.CurrentDomain.GetAssemblies(): print(f" - {assembly.FullName}") if "MinimalNet8Lib" in assembly.FullName: print(f" FOUND MinimalNet8Lib: {assembly.FullName}, Location: {assembly.Location}") print(f"clr.FindAssembly('MinimalNet8Lib'): {clr.FindAssembly('MinimalNet8Lib')}") print(f"hasattr(clr, 'References'): {hasattr(clr, 'References')}") if hasattr(clr, 'References') and clr.References is not None: print(f"Number of clr.References: {len(clr.References)}") else: print("clr.References attribute is missing or None.") except Exception as e: print(f"Error during diagnostics: {e}") import traceback traceback.print_exc() # Continue to type access attempt print("\n--- Attempting to Access .NET Types ---") try: print("Attempting: import MinimalNet8Lib") import MinimalNet8Lib print("Successfully imported MinimalNet8Lib namespace.") print("Attempting: greeter = MinimalNet8Lib.Greeter()") greeter = MinimalNet8Lib.Greeter() # This is where AttributeError typically occurs print(f"Successfully created Greeter instance: {greeter}") print(f"Calling instance method: {greeter.InstanceHello('Python')}") print(f"Calling static method: {MinimalNet8Lib.Greeter.SayHello('Python')}") print("\nSUCCESS: .NET 8 types accessed successfully!") except AttributeError as ae: print(f"FAILURE: AttributeError: {ae}") print("This indicates types from the .NET 8 assembly were not exposed correctly.") import traceback traceback.print_exc() except Exception as e: print(f"FAILURE: An unexpected error occurred: {e}") import traceback traceback.print_exc() print("\nScript finished.")
-
Run the Python script:
python test_minimal_net8.py
This will show theAttributeError
. -
Test with .NET 9 (Works):
- Change
TargetFramework
inMinimalNet8Lib.csproj
tonet9.0
. - Rebuild:
dotnet build -c Release
. - Update
dll_path
andruntime_config_path
intest_minimal_net8.py
to point to thenet9.0
outputs. - Ensure .NET 9 SDK/Runtime is installed and discoverable.
- Run
test_minimal_net8.py
. The script should complete successfully, accessing the .NET types.
- Change
Additional Notes:
- The issue has been observed with
pythonnet
versions 3.0.3 and 3.0.5. - The problem is reproducible with both a minimal class library and more complex, real-world libraries (e.g.,
Tableau.Migration.dll
v5.0.1, which targets onlynet8.0
and usespythonnet.load("coreclr")
in its__init__.py
, leading to anImportError
when its Python submodules try to import the .NET types). - Setting
PYTHONNET_RUNTIME=coreclr
and usingclr_loader.get_coreclr()
followed bypythonnet.set_runtime()
beforeimport clr
is a reliable way to initialize .NET CoreCLR. Simpler methods like justpythonnet.load("coreclr")
orimport clr
can sometimes lead to .NET Framework being loaded if not carefully managed, but the type exposure issue persists even with explicit and correct .NET 8 CoreCLR initialization.
We believe this is a significant issue for users attempting to leverage .NET 8 libraries with Python via pythonnet
. The fact that it works seamlessly with .NET 9 suggests a specific interaction problem with .NET 8.