dynwinrt

September 16, 2026 · View on GitHub

dynwinrt is the native CPython runtime for Python projections generated by dynwinrt-codegen. It supports CPython 3.11–3.14 on Windows x64 and ARM64.

Install

python -m pip install --pre dynwinrt dynwinrt-codegen
dynwinrt-codegen generate --namespace Windows.Foundation --class-name Uri `
  --lang py --output generated_uri

Generated package manifests pin dynwinrt to the exact version of dynwinrt-codegen that produced them. The runtime wheel includes __init__.pyi and py.typed for static type checking.

Generated IReference<T> values are projected as T | None; native values, None, and generated IReference_* wrappers are accepted as inputs.

Async WinRT operations

Generated async methods return typed, asyncio-compatible operation objects:

operation = writer.store_async()
stored_bytes = await operation

task = asyncio.create_task(writer.store_async())
stored_bytes = await task

async with asyncio.TaskGroup() as group:
    task = group.create_task(writer.store_async())

Generated methods return WinRTCoroutine[T] and WinRTCoroutineWithProgress[T, P], which retain the structural WinRTAsync[T] and WinRTAsyncWithProgress[T, P] contracts while also being typed as coroutines for asyncio.create_task(). The concrete runtime wrappers remain private.

Regenerated bindings no longer block inside async methods. Existing code that expects an immediate result must use await operation or operation.wait().

asyncio task cancellation calls IAsyncInfo.Cancel() on the underlying WinRT operation. Operations with supported progress values also expose operation.progress(callback). Fast operations can finish before registration; in that case no future progress exists and registration is a no-op.

An operation can be awaited directly more than once; every direct await observes the same converted completion, failure, or cancellation. Its coroutine driver is one-shot like a native Python coroutine, so pass a given operation to create_task(), TaskGroup.create_task(), or ensure_future() only once. Directly awaiting that operation after its task completes remains supported. Calling close() cancels pending WinRT work and permanently closes the coroutine driver.

For scripts without an event loop, operation.wait() remains available as an explicit blocking API. It rejects started operations when called from a running asyncio loop or an STA thread, where blocking could freeze or deadlock the caller.

WinRT HRESULT failures raise OSError (or a standard OSError subclass) with the signed HRESULT in error.winerror. The exception message preserves restricted WinRT error information when Windows provides it.

Python-native values

Generated collection projections implement the standard collections.abc protocols: vectors behave as sequences, maps as mappings, and WinRT iterables and iterators work with iter() and next(). Mutable vectors support indexing, slicing, assignment, insertion, and deletion; mutable maps support standard mapping assignment and deletion.

Method inputs accept normal Python sequences and mappings in place of compatible WinRT collection interfaces. Byte arrays accept bytes and bytearray; GUID, DateTime, and TimeSpan values use uuid.UUID, datetime.datetime, and datetime.timedelta.

Generated Windows.Storage.Streams.Buffer and IBuffer projections also provide copied byte conversion:

winrt_buffer = IBuffer.from_bytes(bytearray(b"\x00\x01\xff"))
data: bytes = winrt_buffer.to_bytes()

Both directions copy exactly Length bytes. Mutating the input or releasing the WinRT object does not change the returned bytes, and no native buffer pointer is exposed.

Exceptions raised by Python event or delegate callbacks are reported through sys.unraisablehook. The originating WinRT invocation receives 0xA0EE4005 (PYWINRT_E_UNRAISABLE_PYTHON_EXCEPTION) instead of unconditional success. Generated delegate parameters accept normal Python callables. WinRT chooses the callback thread, so callbacks must not assume they run on the registration thread or an asyncio event-loop thread. Keep each token returned by on_* and pass it to the matching off_* when the subscription is no longer needed. For callback-style cleanup, subscribe_* returns an idempotent unsubscribe function. once_* subscribes for at most one callback invocation.

WinRT flags enums are projected as enum.IntFlag. Overloaded methods share one Python name with runtime type/arity dispatch and typing.overload declarations. Activatable runtime classes use normal constructors, for example Uri("https://example.com"). Constructor overloads come only from WinMD ActivatableAttribute and public ComposableAttribute declarations. Classes without that metadata, including system-returned classes and protected-only composition, raise a class-named TypeError on normal construction and their stubs expose no public constructor. Native return values still use the internal _from_native/DynWinRTValue wrapping path.

Raw object projection

Use project_as(value, Type) when metadata returns Object/IInspectable but the application knows the concrete generated type. This is common with XAML APIs such as XamlReader.load() and FrameworkElement.find_name():

from dynwinrt import project_as
from generated.microsoft.ui.xaml.controls import Button, StackPanel
from generated.microsoft.ui.xaml.markup import XamlReader

raw_panel = XamlReader.load(XAML)
if raw_panel is None:
    raise RuntimeError("XamlReader returned no value")
panel = project_as(raw_panel, StackPanel)

raw_button = panel.find_name("Submit")
if raw_button is None:
    raise RuntimeError("Submit was not found")
button = project_as(raw_button, Button)

project_as() accepts generated runtime classes only and borrows its input: the raw value or source wrapper remains valid. The returned wrapper owns the QueryInterface result, participates in the active projected_lifetime_scope(), and preserves the projection identity cache. Classes with a verifiable default-interface IID remain valid projection targets even when metadata exposes them only through Object/IInspectable. Projection always performs QueryInterface, so a static-only declaration cannot produce a wrapper unless the input actually implements that default interface. Incompatible types raise the ordinary WinRT OSError. Static-only metadata classes with no instance surface are not projection targets.

Explicit boxed-value unboxing

Generic WinRT Object/IInspectable results remain raw DynWinRTValue instances. Use unbox_object() only where the application expects a boxed Windows.Foundation.IPropertyValue, such as values from DeviceInformation.properties:

from dynwinrt import unbox_object

raw = device_information.properties["System.Devices.DeviceInstanceId"]
instance_id = unbox_object(raw)

The helper borrows its argument. It maps supported numeric, Boolean, string, character, GUID, and corresponding array property types to Python values; 64-bit integers use int, GUIDs use uuid.UUID, and UInt8Array uses bytes. None stays None. If the object does not implement IPropertyValue, the exact same Python object is returned, so identity and later projection remain intact. Unsupported property types (including DateTime, TimeSpan, geometry, inspectable, and other types) and native getter failures raise an exception.

Use wrapper.as_interface(InterfaceClass) when converting an existing wrapper to an interface view. Use InterfaceClass.from_value(raw) for a raw DynWinRTValue. Do not call the internal _from_native() method from application code.

COM apartments and cleanup

Use RoApartment to initialize COM for a thread and balance every successful initialization:

with RoApartment(0):  # RO_INIT_SINGLETHREADED
    use_winrt()

Use RoApartment(1) for RO_INIT_MULTITHREADED. Nested contexts using the same model are supported. Requesting a conflicting model raises OSError with RPC_E_CHANGED_MODE. The low-level ro_initialize() API remains available, but each successful call, including S_FALSE, must be paired with one ro_uninitialize() call on the same thread.

Generated runtime classes that implement IClosable support with and an idempotent close() method. Prefer deterministic cleanup instead of relying on Python garbage collection.

Standalone WinRT interface implementations

Generated, supported non-generic WinRT interfaces provide .implementation() and .implement() factories. They create standalone, IInspectable-rooted objects; they do not activate or register an OS class, compose a WinUI control, or implement Classic COM interfaces. .implement() returns a generic management handle with a stable typed .value, not a type cast or the raw handler:

from generated.windows.foundation import IClosable, IStringable

class Label:
    def to_string(self) -> str:
        return "Python-backed WinRT object"

    def close(self) -> None:
        pass

handler = Label()
with IStringable.implement(
    handler, interfaces=[(IClosable, handler)]
) as impl:
    print(impl.value.to_string())

The generated handler protocols describe snake-case methods and their input and output types. Additional interface descriptors provide separate native views sharing the same object identity. Every required interface must be included; an incomplete implementation is rejected before publication. release_projected(view) releases a view independently without disposing or releasing its controller. The advanced from_implementation(impl) path accepts both new handles and low-level native owners; positional descriptors remain supported. The primary .value needs no release_projected in the common pattern. It is created lazily once, and release()/dispose() release that view along with the handle's owner reference. .value raises after either operation and never recreates a released view. Heterogeneous convenience lists have per-pair static checking for interfaces from the same generated package. For heterogeneous combinations across generated packages, use positional typed .implementation(...) descriptors. Runtime list acceptance and the generic homogeneous-list overload are unchanged.

The management implementation ships as dynwinrt/_implementation.py beside the native extension. Tracebacks and coverage use this installed source file; no build checkout is needed. The native module's public class identities and exports are unchanged.

The low-level runtime surface is:

  • DynWinRTImplementationMethod(name, vtable_index, signature): an immutable method definition using the existing DynWinRTMethodSig.
  • DynWinRTInterfacePlan.create(name, interface_type, methods, required_iids=()): an immutable, validated interface definition. Methods occupy every slot starting at slot 6; required-interface membership is checked when composing the complete implementation.
  • DynWinRTImplementationDescriptor(plan, dispatch): a generated interface plan and synchronous dispatch(vtable_index, args) callable.
  • DynWinRTImplementation.create(interfaces, callback, runtime_class_name=None): the native owner. Its callback receives (interface_index, vtable_index, list[DynWinRTValue]) and must return a list[DynWinRTValue] in signature output order, with any logical return last. A void callback returns [], not None.

Prefer generated plans rather than hand-authoring native signatures. The runtime validates scalar, GUID, HSTRING, complete struct, managed-reference, and pass/receive/fill-array contracts. For fill arrays the callback receives a UInt32 capacity and must return an array of exactly that length. Existing outbound DynWinRTMethodHandle calls still take a preallocated array value for a fill-array argument. DynWinRTValue.from_hresult() constructs an exact signed HRESULT value, including for typed HRESULT arrays.

Callbacks are synchronous and run only on the creating native thread, with a fresh copy of the contextvars context captured at creation. Reentrant calls are supported. Async callables and coroutine/awaitable results are rejected; no event loop is scheduled or blocked. Already-created native WinRT async values can be passed through like other managed native values. Arbitrary generic interface implementations, cross-thread dispatch, and Python subinterpreters are not supported.

Callable wrappers for received native delegates can prepare a reusable DynWinRTDelegateMethod.create(iid, signature) and call method.invoke(value, args). The convenience value.invoke_delegate(iid, signature, args) remains available and uses the same invocation path. The IID and signature must describe the same WinRT delegate's Invoke method, including computed closed-generic IIDs. This calls slot 3 on the delegate's IUnknown root and returns all outputs as a list ([] for void). It borrows the Python value, owns a native pin through the call, and propagates failed HRESULTs. Arguments use the existing outbound invoke_all() contract, including preallocated fill-array values; these helpers do not infer signatures or turn arbitrary objects into delegates. Generated received-delegate callables use positional-only arguments in both their Protocol declarations and runtime wrappers; ordinary projected methods continue to accept their existing keyword arguments.

Implementation lifetime and callback failures

to_value() returns an independently owned native reference. release() drops only the owner's reference; callbacks remain alive while native consumers retain other references, even after the Python owner wrapper is gone. disconnect() disables all views and releases captured Python handlers without dropping the owner's native reference. dispose() disconnects and releases, and is repeat-safe. The owner context manager calls dispose(). An in-flight callback can finish safely when it calls any of these methods; subsequent calls after disconnection fail with RO_E_CLOSED.

GC tracing is deliberately conservative: captured Python references are reported as owner edges only when that owner is the sole native reference. This collects ordinary handler/self/owner cycles without treating native consumers as collectable Python references. Cycles that retain a native alias, or that have requested native weak references, may require explicit dispose(), including a handler capturing a typed handle whose primary view has already been created. Callback invocation and to_value() require the creating thread; foreign-thread to_value() raises OSError with RPC_E_WRONG_THREAD. Owner release, disposal, and garbage collection can run on another Python thread without leaking the owner's native reference. This does not make arbitrary wrapped Windows objects agile. Prefer deterministic cleanup; unexpected GC cleanup failures are reported to sys.unraisablehook. Interpreter shutdown separately closes the callback gate and drops captured handlers; late native calls cannot enter a finalized interpreter.

Python callback exceptions and invalid Python output containers are reported to sys.unraisablehook and fail the native call with PYWINRT_E_UNRAISABLE_PYTHON_EXCEPTION (0xA0EE4005). Native output-contract mismatches also fail rather than returning fabricated values. take_error() returns and clears the latest native callback diagnostic; is_closed is a read-only property.

Experimental WinUI support

When the required WinUI metadata is generated, Application.create() installs XamlControlsResources and configures unpackaged resource resolution. Application.create_with_metadata_provider(...) is available when the application supplies its own provider.

Python retains the validated WinUI implementation DLL after its first actual WinUI activation until the process terminates. One named native owner keeps one ordinary loader reference to Microsoft.UI.Xaml.dll; repeated activation does not accumulate references. Retained native module handles determine identity, so legal differences in path spelling or casing do not imply a different runtime. If WinUI activation requires the DLL-probing fallback, its successful loader reference is transferred to the same owner (one reference per required implementation DLL), rather than leaked per call. Imports, ordinary WinRT activation, and COM apartment initialization do not load or retain WinUI or CoreMessaging. When a WinUI metadata provider loads Controls before XAML, the runtime resolves the actual Application activation factory to establish this lifetime before returning the provider's factory; it does not create an Application or start its message loop.

This is a process-lifetime module policy, not an object cache. Continue to close windows, unsubscribe events, release projected/native objects, and balance COM initialization on each thread as shown below. WinUI module-level static state can remain after those application-owned resources are released. The retained XAML mapping keeps its code valid when Controls static destruction runs during COM cleanup, even when another MTA exits after the UI STA.

No new context manager or async executor is required. The Python runtime does not release the published module reference from RoApartment.close(), Python atexit, or wrapper finalizers: the operating system reclaims it at process termination. Consequently, in-process unloading/hot replacement of that WinUI implementation is not supported. This does not prohibit WinUI's documented thread-level XAML/DispatcherQueue shutdown, guarantee repeated Application.start() in one process, or imply that all SDK static resources are gone when a window closes. The JavaScript and independent WinRT/COM lifetime policies are unchanged.

Python subclasses of public composable controls preserve one COM identity for inherited properties and methods. Metadata-supported measure_override, arrange_override, and on_apply_template callbacks run synchronously on the creating UI apartment with the contextvars context captured during construction. Unsupported native override shapes fail during construction instead of falling back to an unsafe ABI.

After creating the generated application, publicly composable controls can register a Python subclass for activation by XamlReader:

from generated.microsoft.ui.xaml.controls import StackPanel
from generated.microsoft.ui.xaml.markup import XamlReader

class PythonPanel(StackPanel):
    def measure_override(self, available_size):
        return available_size

registration = StackPanel.register_xaml_runtime_class(
    "MyApp.Controls.PythonPanel",
    PythonPanel,
)
raw_panel = XamlReader.load(
    '<local:PythonPanel '
    'xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" '
    'xmlns:local="using:MyApp.Controls" />'
)
if raw_panel is None:
    raise RuntimeError("XamlReader returned no value")
panel = StackPanel(raw_panel)

# First remove every instance from the XAML tree and release application owners.
panel = None
raw_panel = None
registration.unregister()
registration.release_instances()

Registrations are process-local, and duplicate names fail. Unregistering, closing, or dropping the registration prevents new XAML metadata lookups. XAML-created Python owners remain rooted until release_instances(); call it only after every corresponding native control has left the XAML tree. Registration does not make the class globally activatable through RoActivateInstance.

Generated Application.start() and DispatcherQueue.run_event_loop() calls stay on the caller's native thread but release the Python GIL while WinUI pumps messages. WinRT callbacks reacquire the GIL, and worker threads can use DispatcherQueue.try_enqueue() to return to the UI thread.

Use a projection lifetime scope inside the COM apartment so wrappers release their native values before RoUninitialize:

from dynwinrt import RoApartment, projected_lifetime_scope

with RoApartment(0), projected_lifetime_scope():
    app = Application.create()
    # Create and use WinUI objects here.

Scopes nest in LIFO order. Wrappers that survive a closed scope remain Python objects, but their native values are released and further WinRT calls fail. Each scope is thread-affine: enter, use, and close it inside that thread's RoApartment. Same-thread asyncio tasks inherit the active scope, while worker threads must open their own ordered with RoApartment(...), projected_lifetime_scope():. Native callbacks invoked on a foreign thread preserve other captured context but do not inherit the creator thread's lifetime scope. This includes generated delegates, raw progress handlers, and element-factory callbacks. Retained callback values remain user-owned; open an explicit callback-local scope for deterministic temporary cleanup.

Normal construction remains unavailable for protected-only composable classes and system-returned classes without public activation metadata. Named Python XAML registration does not support generic names, collection or dictionary bases, markup-extension bases, or Python-defined XAML members.

Develop

From bindings\py:

python -m pip install "maturin>=1.11,<2" "pytest>=8.3.5" "mypy>=1.13,<2"
python -m maturin develop
python -m pytest

The focused WinUI lifetime regressions are in tests\test_winui_lifetime.py. Without optional inputs they check ordinary WinRT imports and cleanup. Set DYNWINRT_WINUI_SAMPLE_ROOT to a prepared Hello World directory (unchanged app.py, generated, and .runtime) to exercise real STA, managed/external MTA, exception, and default-async process teardown. Run each case in its own process; process exit status is part of the assertion.

For native ownership inspection and the test-only shutdownable Tokio fixture, build a separate test environment with maturin develop --features test-hooks. This adds private test helpers only, not a production executor or public API. DYNWINRT_WINUI_REQUIRE_OWNER_HOOK=1 additionally checks the retained owner. From a matching-architecture Visual Studio developer shell, build tests\e2e\fixtures\winui_activation.cpp with cl /LD /EHsc /std:c++17 winui_activation.cpp /link runtimeobject.lib /OUT:Microsoft.UI.Xaml.dll in a private output directory and set DYNWINRT_WINUI_FIXTURE_DIR to it. Copy that fixture DLL as Microsoft.UI.Xaml.Controls.dll in the same directory for the dependency path-spelling regressions. That SDK-typed factory tests actual loader-reference transfer, concurrent fallback activation, failed/null factories, identity mismatch, and retry without requiring WinUI installation. Do not use this fixture DLL in the real sample directory.

Release process

The release tag supplies one unified npm/Cargo version. For example, v0.1.0-preview.21 produces npm version 0.1.0-preview.21 and Python version 0.1.0rc21 after PEP 440 normalization.

  1. GitHub Actions builds and consumes eight CPython 3.11–3.14 runtime wheels and two standalone codegen wheels on Windows x64 and native ARM64.
  2. The official 1ES ADO pipeline builds both npm packages and waits for the complete Python wheel matrix.
  3. ADO creates one shared GitHub Release with the npm tarballs. GitHub Actions attaches the ten tested Python wheels, and ADO downloads and revalidates the complete set.
  4. With DoEsrp enabled, ADO publishes both npm packages. PublishPyPI defaults to enabled and publishes the eight dynwinrt wheels before the two dynwinrt-codegen wheels. Disable it only for a non-PyPI rehearsal.

PyPI publication uses the Microsoft ESRP release identity and is not available from GitHub Actions.