In-process CPython via pythonnet. Source: source/DevTools.Execution/Providers/Python/.
This page is the constraint record. Keep source comments to one-line API summaries.
PythonInitializer.InitializeAsync:
- Take
InitLock(WaitAsync). - If already initialized, return.
- Probe the host (
TryGetHostPythonDll). SetHostOwnsInterpreter. - If host DLL: snapshot version onto uv and pip (
AttachHostInterpreter), thenInitializeEngine(hostDll)— must stay beforeResolveProviderAsync. ResolveProviderAsync: host owns interpreter → uv, else pixi. If that manager exe cannot run → pip (pyRevitcengines). Pip does not change who owns the interpreter.- If no host DLL:
InitializeEngine(provider)(pixi or pip). SetupRuntime.
flowchart TD
Probe["TryGetHostPythonDll?"]
Probe -->|yes| BindH["AttachHostInterpreter + InitializeEngine hostDll"]
BindH --> Uv["uv Setup"]
Probe -->|no| Pixi["pixi Setup"]
Uv -->|ok| Complete
Uv -->|fail| PipH["pip sidecar same version"]
PipH --> Complete
Pixi -->|ok| BindO["InitializeEngine provider"]
Pixi -->|fail| PipO["pip owns cengines"]
PipO --> BindO
BindO --> Complete["SetupRuntime"]
Two axes, not one chain:
| Axis | Question | Result |
|---|---|---|
| Interpreter | Is CPython already in this process? | Yes → attach, never map a second PythonDLL. No → this process owns pixi or pip. |
| Packages | Can uv.exe / pixi.exe run? |
Yes → that manager. No → pyRevit cengines + python -m pip. |
Host-attach + pip only works when the cengine version equals the host (python312.dll vs python313.dll). A different pyRevit engine cannot load .pyd / encodings into the host interp — that was CPY3123 on Plant 3D 3.13. Own-process + pip: any ready CPY* is the interpreter, so its version need not match a host. Version is read from python3xx.dll, not from the folder name.
InitializeEngine is the only PythonEngine.Initialize() site. Two prologues, then ProgramName / Initialize / BeginAllowThreads:
| Process | Prologue | Must not |
|---|---|---|
| Host already initialized CPython | Runtime.PythonDLL = hostDll; ClearPythonnetStash |
Set PythonHome; map a sidecar DLL |
| This process owns CPython | PrepareProcess(home); GetPythonDllPath(); PythonEngine.PythonHome = home |
Attach a second interpreter |
SetupRuntime (GIL held): if this process owns CPython, os.add_dll_directory the prefix. If the host owns it, overlay sidecar stdlib (DLLs then Lib), encodings.__path__, then LoadStableAbiForwarder (sidecar python3.dll by full path). Then InjectSitePackages, global scope, debugpy.
PythonBackend lives in Models/PythonBackend.cs (Pixi / Uv / Pip). Keyed DI on PyEnvironmentProvider uses those values.
Do not “fix” these in source without updating this page.
Host RunBlocking does not pump. Any hop before PythonEngine.Initialize() lands on the thread pool: AV (InitExt, no GIL) or deadlock (pending-call / GIL). Host-attach therefore runs InitializeEngine(hostDll) before ResolveProviderAsync. Do not add InitExt / Py_AddPendingCall. Init takes the lock with WaitAsync (not Wait(0)); do not insert other awaits between lock acquire and host InitializeEngine.
Plant 3D and similar may load python3xx.dll and call Py_Initialize before the add-in. A second in-process CPython (e.g. pixi python314.dll) is an ABI crash.
- Probe loaded modules only (
Py_IsInitialized). Do not touch pythonnet first, orFromEnv/ Pixi DLL load will mismatch. - Bind pythonnet to the versioned interpreter (
python313.dll), not Windowspython3.dll. That file is a PEP 384 stable-ABI forwarder: subset of the C API, same filename on 3.11 / 3.13 / pixi. ClearPythonnetStash: take the host GIL and deletesys.clr_dataso pythonnet does not restore aNoopFormatterstash.- Do not
Py_Finalize/PythonEngine.Shutdown— that tears down the host interpreter (use-after-free on later host Python and CAD exit). - Do not
os.add_dll_directorya pixi/condaLibrary\bininto the host interp (OpenSSL vs Autodesk stub DLLs). Do notadd_dll_directorythe uv prefix either —.pydfiles load withLOAD_WITH_ALTERED_SEARCH_PATHfromDLLs. - PEP 723 installs into the sidecar, never into the host prefix: uv
uv-env\{major.minor}, or pip matchingcengines\CPY*whenuv.execannot run.site.addsitedirthatLib\site-packageslast (packages only — do not setPYTHONHOME/sys.prefixto the sidecar). - Plant 3D host stdlib is
python313.zipnext topython313.dll(not aLibdirectory). Observed mix:stringprepfrom the zip,unicodedata/selectfrom sidecarDLLs. Append matching-version sidecarDLLsthenLib(pyvenv.cfghome) ontosys.path. Never put the sidecar base (the dir withpython313.dll) onsys.path.encodingsis already imported from the zip, so append sidecarLib/encodingstoencodings.__path__(submodules do not consultsys.path). abi3 wheels (polars runtime) importpython3.dll; the host process often only mappedpython3xx.dll.LoadLibrarythe matching sidecarpython3.dllby full path so the loader bindspython313.dllto the already-loaded host module — do notLoadLibrarysidecarpython313.dll. Do not register individual codecs.uv python install --no-binso scoop/user shims are not replaced.
HostOwnsInterpreter skips Py_Finalize / PythonEngine.Shutdown and os.add_dll_directory. Overlay + python3.dll load still run.
Revit (and some Autodesk hosts) ship empty libssl-3-x64.dll / libcrypto-3-x64.dll next to the exe. Those win the default loader search over conda Library\bin, so in-process import ssl fails with Win32 193.
PrepareProcess (owned interpreter only): prepend conda Library\bin to PATH, AddDllDirectory, then preload crypto then ssl by absolute path before PythonEngine.Initialize(). Env root and DLLs are not search dirs — pythonnet loads python3xx.dll by full path; adding those folders does not beat exe-adjacent stubs.
PyEnvironmentProvider.VerifyRunnableAsync runs {ManagerExePath} --version. Pixi and uv set ManagerExePath to their AppData exe; pip has none. Installers only download. Failure of the chosen manager opens the pip step (sidecar if host-attached, owned interpreter otherwise).
| Backend | When | Role | Prefix |
|---|---|---|---|
| uv | Host owns interpreter | PyPI sidecar keyed to host major.minor (python313.dll → 3.13 / cp313). Never mapped as in-process PythonDLL when attached. Layout: uv-env\{major.minor} venv, uv-env\uv-python, uv-env\uv-cache. |
%APPDATA%\RevitDevTool\uv-env\{major.minor} |
| Pixi | No host interpreter | Owns in-process CPython (conda-forge, then PyPI). Bootstrap lives in SetupEnvironmentAsync. |
%APPDATA%\RevitDevTool\pixi-env\.pixi\envs\default |
| pip | uv.exe or pixi.exe cannot run |
pyRevit cengines + python -m pip. Host-attach sidecar only if engine version equals host. Own-process: first ready CPY* is PythonDLL (any version). |
engine dir with python.exe |
IsEnvironmentReady() is File.Exists(PythonExe) for pixi/pip. uv also requires pyvenv.cfg home to still contain python.exe (a leftover trampoline after moving uv-python is not ready). Stale venvs are recreated with uv venv --clear. Locked installers (PixiInstaller, UvInstaller) live under AppData bin.
Package policy: skip-if-listed for all backends. Pixi missing specs use search-first conda vs PyPI (0014); argv lives in PixiArgs (install / add [--pypi] / search / list). uv package ops are uv.exe pip … --python <venv> via UvArgs (not python -m pip). Pip is pyRevit cengines when those manager exes cannot run — sidecar if the host already owns CPython, interpreter if not.
PythonEmbedded extracts Parser.py, ToolParser.py, PytestRunner.py, setup scripts, and pixi.toml. Parser is overwritten every load; pixi.toml is not if present. Manifest names are resolved across DevTools.Execution.dll and ILRepack-merged host layouts (AcadDevTool.DevTools.Execution.Resources.scripts.*). Session ~/.pixi/config.toml sets tls-root-certs = "system" for corporate CAs. Pixi search uses exit code only — --json with --limit can dump multi-MB. CLI capture goes through ExecuteBufferedAsync (RunPixiBufferedAsync / RunUvBufferedAsync). Line ToDelegate is forbidden for JSON: pixi list --json / uv pip list --format=json / pip list --format=json is one large line; a line pipe fills (~64KB) and deadlocks WaitForExit + CopyFromAsync while OnStartup is in RunBlocking. RunPixiAsync / RunUvAsync replay log lines after exit. Package stores list via GetListJsonAsync (pixi UI list uses ListExplicitJson() on the same buffered helper).
PEP 723 parsing: PythonDepsManager through Parser.py. Installed-state JSON may include conda git-describe versions; Parser treats those as unconstrained.
- Lasting policy (interpreter owner vs package manager): 0030
- Orchestrator / package UI: code-execution.md (
IPythonPackageStorefor the current backend) - Pytest CPython
tests/runuses this same initializer: pytest-bridge.md - Product contract: product/execution.md
- Agent digest: execution-system.md