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 existingDynWinRTMethodSig.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 synchronousdispatch(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 alist[DynWinRTValue]in signature output order, with any logical return last. A void callback returns[], notNone.
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.
- GitHub Actions builds and consumes eight CPython 3.11–3.14 runtime wheels and two standalone codegen wheels on Windows x64 and native ARM64.
- The official 1ES ADO pipeline builds both npm packages and waits for the complete Python wheel matrix.
- 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.
- With
DoEsrpenabled, ADO publishes both npm packages.PublishPyPIdefaults to enabled and publishes the eightdynwinrtwheels before the twodynwinrt-codegenwheels. Disable it only for a non-PyPI rehearsal.
PyPI publication uses the Microsoft ESRP release identity and is not available from GitHub Actions.