Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

dynwinrt

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. Do not mix generated bindings with an older runtime wheel. Upgrade to the matching runtime version and regenerate all Python bindings together; generated interface overloads fail explicitly when a required native guard is missing. Generated Python packages record the full producer version separately. Upgrades, downgrades, and legacy unstamped output require manually cleaning the dedicated codegen output and regenerating the complete bindings selection; incompatible generation fails without changing existing output. See the producer-version and path contracts.

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

Nullability in type stubs

WinRT metadata does not record which values can be null, and most APIs raise an exception instead of returning null. The generated .pyi stubs therefore type the values you receive as non-null by default: method and property results, async results and out values. For example, StorageFolder.create_file_async() returns WinRTCoroutine[StorageFile].

These values keep | None:

  • IReference<T> values, projected as T | None everywhere;
  • results of Try* members, such as try_get_item_async() or JsonObject.try_parse(), where null means "not found";
  • results of Windows SDK members whose documentation says they can return null, such as Accelerometer.get_default(), DispatcherQueue.get_for_current_thread() or StorageFolder.get_parent_async(). The codegen embeds this list, derived from the Windows SDK API reference; it does not cover Windows App SDK (Microsoft.*) APIs;
  • Object/IInspectable values (DynWinRTValue | None) and delegate-typed values, which are often null.

Windows.Data.Xml.Dom also keeps | None for an absent document root or DTD, a node's owner document, parent and previous/next sibling, missing attribute lookups (including namespace-aware lookups), and the previous attribute returned by set_attribute_node()/set_attribute_node_ns(). Cache the result and guard it before reading a member:

root = document.document_element
if root is not None:
    print(root.tag_name)

These are exact reviewed null-result facts, not a blanket rule for WinRT references. They tighten the static declarations; native/runtime null conversion and the broader inline .py/--no-pyi annotations are unchanged. Class documentation facts also have exact declaring-interface aliases for standalone rendering; this does not enable exclusive interfaces such as IXmlDocument or IXmlElement as public CLI roots.

DataWriter.detach_stream() and DataReader.detach_stream() return IOutputStream | None and IInputStream | None, respectively, including their declaring-interface projections. An unbound DataWriter() or a DataReader.from_buffer(...) has no stream to detach; a second detach also returns None. Save the result and check it before calling stream methods:

stream = writer.detach_stream()
if stream is not None:
    await stream.flush_async()

Reference-type elements read from WinRT collection interfaces are always typed T | None, including vectors, views, iterables, iterators, map keys and values, and key-value-pair keys and values. Here reference means the projection's supported COM-pointer shapes: Object, interfaces, runtime classes, delegates, and parameterized interfaces. Async wrappers are not collection element shapes. Reference-type elements of returned WinRT arrays are also T | None; the array value itself remains non-null. String, GUID, scalar, enum, and struct array elements and keys remain non-null. A view or iterator obtained from a mutable collection can expose a null slot, and WinRT collection interfaces do not retain enough provenance for the stubs to distinguish that case. For example, a custom IVector<IJsonValue> can hold IJsonValue | None, and get_files_async() returns WinRTCoroutine[Sequence[StorageFile | None]]. Value-type elements remain non-null.

Mutable collections accept None when their element, map-key, or map-value type is a WinRT reference type and store a real null WinRT value. This includes append(), insert(), index and slice assignment, extend(), update() and setdefault(). String, GUID, scalar, enum, and struct keys and value-type elements reject None with TypeError.

Stock Windows.Data.Json.JsonArray and JsonObject are exceptions: their native APIs reject a null IJsonValue pointer. Generated wrappers check the receiver's native class before mutation, including generic IVector<IJsonValue> and IMap<String, IJsonValue> views, raw DynWinRTArray inputs, replace_all, slice assignment, extend and update. A rejected null leaves the JSON collection unchanged. Use JsonValue.create_null_value() to store JSON semantic null; it is a non-null IJsonValue object. Their class stubs type elements as non-null, and the generated .py input annotations agree even with --no-pyi. Generic interface annotations retain | None because custom implementations can store a native null and a view's origin is only known at runtime.

Other arguments keep accepting None where they did before. The stubs are optimistic, like the generated TypeScript declarations: the runtime still returns None when a WinRT API returns null, so check the API documentation when a result can legitimately be absent. The inline annotations of the generated .py modules, which typing.get_type_hints() and --no-pyi output expose, still mark every object result | None.

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.

Bulk writes use the same checked inputs as item assignment: update() accepts mappings or iterables of key/value pairs, plus keyword entries for string-keyed maps; extend() and += accept iterables of valid vector inputs. They do not change the types returned by reads. extend() returns None, and += returns the same wrapper.

If metadata already exposes a native member with a helper's Python name, that native method, property or compatibility alias keeps its existing meaning. Use an explicit collection interface view for the standard collection helper instead; generated helper refinements never replace a native entry point. When a vector has a native extend, += keeps its inherited dispatch to that member rather than gaining a new bulk-write refinement.

setdefault(key, default) returns the value as self[key] reads it, including after inserting a missing key. For an Object-valued map such as PropertySet, a generated wrapper with a valid _obj can be a write input, but reads and setdefault() still return DynWinRTValue | None, not that input wrapper or an automatically unboxed Python value. The input remains usable. A present key leaves its default unused, while a missing key requires a valid default (None can be omitted only for nullable value types). Explicit fallback defaults passed to get() or pop() are not stored and remain unchanged. Native failures propagate; ordinary bulk writes can retain entries already written before a later failure. The stock JSON null preflight described above still rejects null inputs before mutation.

Sequence containment, index(), count(), and remove(), mapping equality, and value/item-view membership compare WinRT references by canonical COM identity, including different generated class/interface views and raw DynWinRTValue carriers of the same object. This is reference identity, not a runtime class's equals() or content comparison. Python scalars and generated structs retain their normal equality, and released or wrong-thread native access still raises. Wrapper/value == and hashing outside these collection operations are unchanged. Mapping equality also preserves entry counts: distinct Python keys are not collapsed into one entry just because they alias the same native reference.

Raw Object collections do not implicitly box or unbox for comparison: different boxes remain different references. An opt-in object_value_view() compares its converted Python values normally and any remaining native references by COM identity.

Method inputs accept normal Python sequences and mappings in place of compatible WinRT collection interfaces, subject to the producer limits below. 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.

Byte conversion can be repeated in independent RoApartment scopes. Buffer creation uses a call-local activation factory, so it does not retain a stale factory across apartment teardown. Release native values before leaving their apartment, or let RoApartment release its tracked owners:

from dynwinrt import DynWinRTValue, RoApartment

for _ in range(2):
    with RoApartment():
        value = DynWinRTValue.from_bytes(b"abc")
        data = value.to_bytes()
        value.release()

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.

A Python callable passed as a delegate (an event handler, a callback parameter such as ThreadPool.run_async(handler), or a delegate-typed property) receives the delegate's arguments as projected Python values, typed from the delegate's Invoke signature. WinRT Object arguments stay DynWinRTValue | None, and IReference<T> arguments are native values or None. Delegate-typed callback arguments are raw DynWinRTValue | None because a null native delegate is passed to the callable as None. Async-operation arguments stay raw DynWinRTValue objects so a callback projection cannot take over or cancel the operation's completion. For example, map_changed handlers of PropertySet, StringMap, ValueSet, and other IObservableMap<K, V> implementations receive the IObservableMap<K, V> projection, which is a mutable mapping, and an IMapChangedEventArgs<K> with collection_change and key. An existing native delegate, such as one built with DynWinRtDelegate.create, is passed through unchanged, and its callback keeps receiving raw DynWinRTValue arguments. on_* and subscribe_* accept those native delegates. once_* requires a Python callable because it must wrap the callback to remove the subscription after the first invocation.

Delegate-accepting constructors accept the DynWinRtDelegate object, but their stubs intentionally do not advertise its raw DynWinRTValue. A single raw value passed to a runtime class is reserved for wrapping an existing native instance before constructor overload dispatch. Keep the delegate object for a constructor, or pass the raw delegate to a named factory/method instead.

Callback parameter annotations are non-null by default except for Object, IReference<T>, and delegate-typed arguments. This is an intentionally optimistic typing policy, not a guarantee from the Invoke metadata: WinMD carries no nullability information, and the runtime still passes None when WinRT supplies a null reference. Precise callback signatures live in the generated .pyi contract. Executable .py methods use the cycle-safe runtime annotation Callable[..., object] | DynWinRTValue | DynWinRtDelegate, so typing.get_type_hints() resolves without eagerly importing the callback argument graph.

Projected callback arguments are created outside the lifetime scope that was active when the callback was subscribed. Short-lived arguments are therefore released when their Python wrappers are dropped instead of accumulating in a long-lived UI/application scope. If a callback retains an argument, its wrapper owns the native reference and remains valid after the callback and after that subscription-time scope closes; drop it normally or call release_projected() when deterministic release is needed.

def changed(sender: IObservableMap_String_Object, args: IMapChangedEventArgs_String) -> None:
    if args.collection_change == CollectionChange.ItemInserted:
        print(args.key, sender[args.key])

unsubscribe = properties.subscribe_map_changed(changed)

Async notification handlers

Use the public AsyncEventScope to attach an async handler to an existing subscribe_* method:

import asyncio
from dynwinrt import AsyncEventScope, RoApartment, projected_lifetime_scope
from generated.windows.foundation.collections import (
    IMapChangedEventArgs_String, IObservableMap_String_Object, PropertySet,
)

async def on_changed(
    sender: IObservableMap_String_Object, args: IMapChangedEventArgs_String,
) -> None:
    await asyncio.sleep(0)
    print(args.key, len(sender))

async def main() -> None:
    with RoApartment(), projected_lifetime_scope():
        properties = PropertySet()
        async with AsyncEventScope(loop=asyncio.get_running_loop()) as events:
            unsubscribe = events.subscribe(properties.subscribe_map_changed, on_changed)
            properties["first"] = None
            unsubscribe()  # Stop future notifications; the accepted task still runs.
            properties["not-delivered"] = None
        # Subscriptions are removed and all accepted tasks have finished here.
        # Only now may the projection scope and COM apartment close.

asyncio.run(main())

The async-events sample runs real PropertySet.MapChanged notifications and native asynchronous file I/O, including an optional handler-failure path.

AsyncEventScope composes asyncio.TaskGroup: normal exit first stops accepting notifications and removes subscriptions, then waits for accepted tasks without cancelling them. A handler failure or exception in the body cancels siblings and waits for their finally blocks before propagating the standard task-group exception; external cancellation also waits for cleanup. A handler's own CancelledError has the standard TaskGroup meaning, not an instruction to cancel all siblings. Handlers run concurrently; completion order is not guaranteed, and there is no backpressure, throttling or retry policy. Handlers must cooperate with cancellation.

The returned unsubscribe callable is idempotent after successful removal and does not cancel accepted tasks. A native removal failure is raised to its caller, leaves delivery disabled, and can be retried with the same callable on the owner loop/thread while native owners are still live. Scope exit attempts every remaining removal in reverse registration order. Removal failures (and non-cancellation body exceptions) are preserved in an exception group passed to TaskGroup, cancelling outstanding work rather than hanging in a normal drain. A body's CancelledError is TaskGroup control flow, not an extra leaf added to that cleanup error group. When cancellation coincides with other errors, their propagation and cancellation bookkeeping follow the running Python version's TaskGroup semantics. Save unsubscribe callables if your application needs to recover from native removal failures; recovery must happen before closing native lifetimes.

The scope is single-use and must exit in the task that entered it. Its constructor binds it to the creating OS thread; entry, subscription, callbacks, task execution, and unsubscription require that thread and the supplied actually running loop. Keep the outer RoApartment and any captured projected_lifetime_scope() alive through task cleanup. Existing subscription context-variable and projection-lifetime behavior is preserved, not bypassed. Wrong-thread delivery fails through the existing synchronous native callback error path (sys.unraisablehook and a failing delegate HRESULT); the event source decides whether that HRESULT fails the originating invocation. No native arguments are marshalled or queued to another thread. Registration and task-creation failures likewise remain synchronous callback/subscription errors.

This is only for ordinary same-thread notifications whose work may be deferred. The application must drive asyncio: the scope does not create a loop, start a thread, integrate a WinUI message pump, or make a blocking Application.start() advance asyncio. It does not implement synchronous return values, Handled, Cancel, or deferral semantics, and is not a general async adapter for WinUI buttons or all Windows events. With an eager task factory, a handler may start before the native callback returns, including during subscription. Native reentrancy restrictions still apply: for example, yield before mutating the same PropertySet from its handler when using an eager factory.

The handler may be an async def or a callable returning an awaitable; it is called inside an owned task and its result is awaited. Passing a bare async def directly to the old subscribe_* API instead merely creates an unawaited coroutine. Existing synchronous callbacks, native delegates, and synchronous callbacks returning an already TaskGroup-owned task are unchanged.

Async-operation arguments deliberately stay raw. A work item can inspect its IAsyncInfo status without installing another completion handler:

from dynwinrt import DynWinRTValue, release_projected
from generated.windows.foundation import AsyncStatus, IAsyncInfo
from generated.windows.system.threading import ThreadPool

def work(action: DynWinRTValue) -> None:
    info = IAsyncInfo.from_value(action)
    try:
        if info.status == AsyncStatus.Canceled:
            return
        # Do work, polling info.status when cooperative cancellation is needed.
    finally:
        release_projected(info)

operation = ThreadPool.run_async(work)

The low-level DynWinRTValue.call_0() and call_1() helpers require an Object holding the intended interface, not a raw Async value. They reject an Async receiver with RuntimeError before native dispatch: its IAsyncInfo identity is not proof of the caller-supplied vtable slot and signature. Cast to the specific interface IID first when making a metadata-checked low-level call, or use the generated async wrapper and its wait()/await API.

ThreadPool.run_async(handler) intentionally retains its original single argument and exact callback annotation, so mypy can infer the type of an unannotated callback lambda. Its priority and options overloads remain separate as run_with_priority_async(handler, priority) and run_with_priority_and_options_async(handler, priority, options). These are distinct names in both generated Python and .pyi; passing priority or options to run_async is not supported.

WinRT flags enums are projected as enum.IntFlag. Most overloaded methods share one Python name with runtime type/arity dispatch and typing.overload declarations. That name is the documented (CLR) method name, so StorageFile.CopyAsync overloads are all copy_async(...). The unique [Overload] ABI names emitted by earlier releases, such as copy_overload or launch_file_with_options_async, remain available as compatibility aliases that keep calling the same overload. A method keeps its earlier name when the documented name would clash with another member, such as a property or the generated close(). Interface parameters of overloads accept any object that implements the interface, such as a runtime class instance or a DynWinRTValue, including a raw async operation that implements IAsyncInfo. Native QueryInterface failures other than E_NOINTERFACE propagate instead of silently choosing another overload. 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.

When no constructor matches, the TypeError lists the supplied argument types and keyword names, followed by all candidate signatures using the same Python names and types as the stubs. For example, Uri(url="https://example.com") reports Uri(url=str), while Uri(42) reports Uri(int); both list Uri(uri: str) and Uri(base_uri: str, relative_uri: str). Use uri=, or supply both base_uri= and relative_uri= for the two-argument overload. Argument values are not included in this diagnostic. Matching calls and errors from native activation or argument conversion are unchanged; named factories such as Uri.create_uri(uri=...) also retain their explicit signatures.

Creating WinRT collections

DynWinRTValue.create_vector(items, element_type) and DynWinRTValue.create_map(keys, values, key_type, value_type) keep their public signatures. They validate element identity, native layout, ownership, and the complete set of closed collection IIDs before creating a native object. These limits apply to producing collections, including conversion of Python sequences/mappings, not to consuming collections returned by Windows.

Element / key / value x64 ARM64 i686
Boolean, integers, Char16, enum, HRESULT Supported Supported Up to 4 bytes; I64/U64 rejected
HSTRING; owned nullable interface/object references Supported Supported Supported
POD structs in populated vectors or maps Sizes 1, 2, 4, 8 Up to 8 bytes, non-HFA only Up to 4 bytes
Additional POD structs in empty vectors only Larger than 8 bytes Larger than 16 bytes, non-HFA only None
Top-level F32/F64/GUID; structs recursively containing HSTRING or references Rejected, even empty Rejected, even empty Rejected, even empty

POD means a validated, reference-free native layout. HFA means an aggregate of one to four same-width floating-point values, including nested aggregates. Thus Point/Size work on x64 but are rejected on ARM64 even when empty; PointInt32 works on x64 and ARM64. ARM64 also rejects empty RectInt32, Rect, and BasicGeoposition vectors. ManipulationDelta (20 bytes, five floats, non-HFA) supports empty vectors on x64 and ARM64. Large map keys/values are rejected even for empty maps.

Structs and typed enums require exact type identity, not a matching byte size or shape. Scalars require the matching value variant, except U16 inputs for Char16, I32 inputs for signed enum/HRESULT, U32 inputs for unsigned enum, and range-checked I32 inputs for I8/U8/Char16. For example, from_i32(255) is accepted for U8; from_i32(257) raises instead of wrapping. Reference inputs are retained and queried for the declared IID; an incompatible interface raises, while DynWinRTValue.null_value() remains a null reference.

WinRT enums preserve their metadata-declared Int32 or UInt32 backing type. DynWinRTType.enum_type(name, names, values) remains signed by default. Pass DynWinRTType.u32_type() as its fourth argument for a UInt32 flags enum; generated wrappers supply this automatically. Enum member lookup, enum_value(), get_enum_int(), to_number(), and to_int() preserve values through 0xffffffff. Unsigned enum arrays and struct fields use the U32 accessors, and their generic interface IIDs include the u4 signature. Update the runtime and regenerate wrappers together.

For admitted POD structs, vector IndexOf and map key operations compare metadata-declared fields by value, including nested fields, and ignore padding. Floating fields use numerical equality: +0 and -0 match, while NaN does not match even the same NaN bits. This also applies to collection views; a NaN-containing struct map key is not found by lookup. Stored field bits are not normalized. Map construction uses the same comparison as Insert: the last value for an equal key wins, retaining the first key's field bits and iteration position.

The Python helpers use validated typed core factories. Rust callers can use the safe create_vector_from_values / create_map_from_values factories for the same checks. The metadata-free Rust create_value_vector, create_vector, and create_map constructors require unsafe: callers must prove the native ABI, ownership, element types, and complete IID set themselves. The metadata-free create_value_vector retains packed-byte equality, including padding, rather than metadata-directed field equality. Unsupported layouts raise RuntimeError; HRESULT-backed type, range, or QueryInterface failures raise OSError. Complete native struct collection support is tracked in microsoft/dynwinrt#161.

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 boxing and unboxing of Object values

Generated Microsoft.UI.Xaml.ResourceDictionary and Windows.UI.Xaml.ResourceDictionary projections, and their WinMD-derived classes such as Windows.UI.Xaml.ColorPaletteResources, accept string keys directly:

raw_style = resources.lookup("AccentButtonStyle")
raw_style = resources["AccentButtonStyle"]
raw_style = resources.get("AccentButtonStyle")

Their key-taking lookup, membership, insert, update and removal operations box str as a WinRT string internally. String subclasses, including StrEnum and str/Enum members, use their underlying string contents rather than custom __str__() formatting. Existing raw or projected-object keys remain accepted, and results and enumerated keys remain raw DynWinRTValue objects (or None where already supported). lookup() preserves native errors; missing [] keys raise KeyError, and get() keeps its normal default. The get() and pop() result annotations include the supplied default type in both .pyi and inline-only (--no-pyi) output. WinMD-derived projections retain the base dictionary's key contract even with an independent default interface. This is a ResourceDictionary-specific input convenience, not automatic boxing for generic IMap<Object, Object>, IMapView<Object, Object> or other Object/IInspectable APIs. Continue boxing their inputs explicitly.

Generic WinRT Object/IInspectable results and parameters remain raw DynWinRTValue instances: an Object may be any runtime object, not only a boxed value. Convert explicitly where the application expects a boxed Windows.Foundation.IPropertyValue, such as the values of DeviceInformation.properties or a PropertySet:

from datetime import datetime, timezone

from dynwinrt import to_winrt_object, unbox_object
from dynwinrt.values import PropertyType, UInt32

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

properties.insert("count", to_winrt_object(5))                   # Int32
properties.insert("port", to_winrt_object(UInt32(8080)))         # UInt32
properties.insert("size", to_winrt_object(2**40, PropertyType.UInt64))
properties.insert("when", to_winrt_object(datetime.now(timezone.utc)))

unbox_object(value, *, preserve_type=False) borrows its argument. None and WinRT null return None. If the object does not implement IPropertyValue, the exact same Python object is returned, so identity and later projection remain intact. Boxed values map to Python values:

  • numbers, Boolean, string and character values use int, float, bool and str; GUIDs use uuid.UUID;
  • DateTime uses a timezone-aware UTC datetime and TimeSpan a timedelta, truncated to microseconds like other positions; a DateTime beyond datetime.max raises OverflowError;
  • Point, Size and Rect use the immutable dynwinrt.values types;
  • UInt8Array uses bytes, other arrays use lists, and InspectableArray elements are unboxed recursively by the same rules.

Empty, Inspectable, OtherType and OtherTypeArray boxes, including nested ones, and native getter failures raise OSError.

With preserve_type=True, values whose plain Python form would box as a different type come back as dynwinrt.values tags (UInt8, Int16, UInt16, UInt32, Int64, UInt64, Single, Char16), and every array except bytes comes back as a typed array such as UInt32Array or InspectableArray. to_winrt_object() then restores the exact PropertyType and value. It creates a new box, so the original box's COM identity is not preserved.

to_winrt_object(value, property_type=None) returns a DynWinRTValue and never guesses a WinRT type:

  • None becomes WinRT null; a DynWinRTValue holding an object is returned unchanged, and a projected wrapper returns its native DynWinRTValue;
  • bool, float, str, a timezone-aware datetime, timedelta, uuid.UUID/WinGUID, bytes/bytearray/memoryview, and dynwinrt.values tags, typed arrays and Point/Size/Rect box as their exact type; a naive datetime raises ValueError;
  • a plain int boxes as Int32 only; other values raise OverflowError;
  • a list or tuple boxes only when all elements have the same unambiguous type, for example all str (StringArray) or all in-range plain int (Int32Array); empty and mixed lists raise TypeError;
  • enum members raise TypeError unless an integer property_type is given. Boxing a WinRT enum as IReference<T> is not supported yet.

property_type accepts a dynwinrt.values.PropertyType member or its integer value and converts the value to exactly that type, validating its range. InspectableArray elements, including None, follow the rules above.

The dynwinrt.values types behave like the plain value: tags compare, hash, format, pickle and serialize to JSON like int, float or str, and arithmetic returns plain numbers. They live in dynwinrt.values rather than dynwinrt, so they never shadow generated Windows.Foundation names such as Point or PropertyType.

Use wrapper.as_interface(InterfaceClass) when converting an existing wrapper to an interface view. Use InterfaceClass.from_value(raw) for a raw DynWinRTValue. as_interface() accepts generated interface classes only; passing a runtime class raises TypeError that points to project_as(). Do not call the internal _from_native() method from application code. Legacy direct construction with InterfaceClass(raw) also checks the interface IID before retaining or caching the pointer. A non-implementing object raises E_NOINTERFACE before any interface method can dispatch; a successful view owns its own QueryInterface reference and does not consume the raw source, even when the view is returned from the identity cache. Stubs require a DynWinRTValue for explicit raw projection, but Python's type system cannot prove its runtime IID.

Views of Object-valued maps

dynwinrt.values.object_value_view(mapping, *, preserve_type=False) is an opt-in view that applies these conversions to the values of a generated map whose values are Object: PropertySet, ValueSet, DeviceInformation.properties, the map returned by StorageItemContentProperties.retrieve_properties_async(), and other IMap<K, Object> or IMapView<K, Object> maps with str or uuid.UUID keys:

from dynwinrt.values import UInt32, object_value_view

view = object_value_view(properties)
view["count"] = 5                    # to_winrt_object(5): Int32
view["port"] = UInt32(8080)          # UInt32
count = view["count"]                # 5
box = view.raw["count"]              # the native DynWinRTValue box

PropertySet and ValueSet map_changed callbacks can also call object_value_view(sender) on their projected IObservableMap<String, Object> sender. The returned view keeps .raw is sender; null entries still read as None.

  • The view is live: every operation goes to the map, and the view holds no WinRT reference of its own. An IMap gets a MutableObjectValueView, a collections.abc.MutableMapping; an IMapView gets a read-only ObjectValueView, a Mapping. Releasing the underlying generated wrapper makes native reads and writes through the view raise the usual released-object error; a live WinRT null entry still reads as None.
  • Reads return None for WinRT null, a runtime object that is not a box as its DynWinRTValue, and unbox_object(value, preserve_type=preserve_type) for a box. Where unbox_object raises for a box without a Python form, the view returns the box's DynWinRTValue, so dict(view) does not fail on one odd entry: an unsupported PropertyType such as OtherType anywhere inside the box, or a DateTime outside the range of datetime. Other errors propagate, including the same HRESULT from a failing getter for an otherwise supported PropertyType. Each read returns a new DynWinRTValue for a runtime object, so compare those with identity_raw().
  • Writes store to_winrt_object(value) and raise its errors unchanged: a plain int boxes as Int32 only, and an empty or mixed list needs a typed array or an explicit property_type.
  • view.raw is the generated map, which keeps returning native DynWinRTValue objects. Use it to keep a box's COM identity: with preserve_type=True, a value that is read and written back keeps its PropertyType, but it is a new box.
  • The generated wrapper declares the exact IMap/IMapView interface used by its mapping methods, and QueryInterface confirms support for that IID. A wrapper for another map on the same COM identity is rejected rather than dispatching through the wrong vtable. Other maps, such as StringMap or JsonObject, raise TypeError; the type stubs reject them too. Runtime-class wrappers generated without this declaration fail closed; reproject them with value.as_interface(IMap_String_Object) (or the Guid/read-only equivalent).
  • Generated IPropertySet wrappers, such as ApplicationDataContainer.values, are not Python mappings. Pass container.values.as_interface(IMap_String_Object) instead.

COM apartments and cleanup

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

from dynwinrt import RO_INIT_SINGLETHREADED, RoApartment

with RoApartment(RO_INIT_SINGLETHREADED):
    use_winrt()

RoApartment() uses RO_INIT_MULTITHREADED, the same as RoApartment(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. ro_uninitialize() rejects calls without a matching dynwinrt.ro_initialize() on that thread, including calls intended to balance an initialization made by another library. Only this library's successful initializations count toward its managed apartment depth.

Do not close the final managed apartment from a synchronous native-to-Python callback (for example, a PropertySet.map_changed handler). close(), __exit__(), and ro_uninitialize() raise RuntimeError before RoUninitialize in that situation. A named RoApartment remains active; wait for both the callback and its outer native call to return, release any retained native callback values, then retry apartment.close() on the owner thread. Non-final nested initializations may still be balanced inside the callback. ro_uninitialize() cannot consume a RoApartment initialization.

If a final RoApartment is dropped during a callback (including an unnamed with RoApartment(): whose __exit__ failed), its initialization stays in a same-thread pending lease rather than being uninitialized inside the native stack. After the native call returns, recover the lease explicitly:

apartment = RoApartment.recover_pending()  # on the original OS thread
# Release any remaining native PropertySet / callback references first.
apartment.close()

recover_pending() raises if called inside a callback or if no lease is pending on that thread; calling it from another thread cannot consume the owner's lease. It does not close or release anything automatically. Retain a named apartment when possible so .close() can simply be retried. A failed owner cleanup is a different pending lease: use retry_pending_apartment_close() (or recover and close the guard) after fixing the release failure. Each pending lease may be claimed only once; the retry helper cannot consume a callback-deferred lease. An implicit drop without an earlier failed close emits a native stderr diagnostic.

An apartment is bound to the OS thread on which RoApartment() was created. Calling __enter__(), close(), or __exit__() on another thread raises RuntimeError without changing its state; retry on the creating thread. If its last Python reference is instead dropped on a different thread, native uninitialization is not attempted there: a native stderr diagnostic identifies the pending lease, which the creating thread can explicitly retrieve using RoApartment.recover_pending() and close after native references are released. Do not depend on Python garbage collection to close an apartment.

If releasing an apartment-owned native callback runs a captured object's finalizer, a successful same-model ro_initialize() in that finalizer remains active after the outer close and must still be paired with ro_uninitialize(). The outer close consumes only its own initialization, not the newly acquired one. Manual closes and pending-close retries reserve their in-progress lease before owner cleanup, including cleanup-failure leases queued by a foreign-thread guard drop. If cleanup fails, that exact lease remains retryable without discarding any initialization or pending lease acquired during cleanup. Owners already released by the drain stay released.

WinRT is never initialized implicitly. A call on a thread without an apartment raises OSError with CO_E_NOTINITIALIZED in error.winerror; its message explains how to open one.

Generated runtime classes that implement IClosable, and interfaces whose WinMD required-interface closure includes it, support with and an idempotent close() helper. This includes IRandomAccessStream, IInputStream, IOutputStream, and IRandomAccessStreamWithContentType; regenerate bindings to use the interface helpers. Existing concrete classes such as DataWriter and InMemoryRandomAccessStream keep the same behavior.

Generate and install the project before using this example:

dynwinrt-codegen generate --lang py --class-name Windows.Storage.StorageFile --output generated
python -m pip install .\generated
from generated import FileAccessMode, StorageFile

async def read_size(path: str) -> int:
    file = await StorageFile.get_file_from_path_async(path)
    with await file.open_async(FileAccessMode.Read) as stream:
        return stream.size

Exiting the context calls native IClosable.Close through QueryInterface, including when the body raises; it does not suppress application exceptions. Successful helper calls mark that projection closed, repeated close() calls are no-ops, and entering it again raises RuntimeError. A failed close remains retryable. Native aliases can observe closure even though their Python projections are distinct.

IClosable itself also supports with; its existing metadata-defined close() keeps its native repeat-call behavior. Helpers never replace an interface's own projected members. If a native member owns close or a context-helper name, use with stream.as_interface(IClosable): instead.

These operations close the native resource; they do not release the projection or substitute for release_projected() / projected_lifetime_scope(). Interfaces without an explicit IClosable requirement and arbitrary Object values do not gain closing methods. 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.

Windows App SDK initialization

The existing initializer supports explicit versions, and optionally a DLL path:

from dynwinrt import init_winappsdk

bootstrap = init_winappsdk(2, 5, bootstrap_dll_path=bootstrap_dll)

init_winappsdk(2, 5) retains its existing behavior and uses WINAPPSDK_BOOTSTRAP_DLL_PATH. Installed generated packages never alter that explicit two-version-argument path.

When bindings were generated with the paired --winappsdk-version / --winappsdk-bootstrap-path options, install their package normally or editable. The same initializer can then discover the installed configuration without application-maintained constants or paths:

from dynwinrt import RoApartment, init_winappsdk, projected_lifetime_scope

with RoApartment(0):  # The application selects the apartment, not the initializer.
    bootstrap = init_winappsdk()
    with projected_lifetime_scope():
        # Create and run the application here.
        ...

Discovery uses the generated package's dynwinrt.winappsdk entry point; no generated-package import is required first. It does not scan application source or infer a version from WinMD names. Missing configuration and conflicting versions/payloads are errors naming the provider packages; identical bundles can coexist. Supplying just one version component is an error.

An explicit bootstrap_dll_path overrides the environment, which overrides the generated resource path. Invalid overrides fail rather than silently falling back. A path override alone still needs a generated version configuration. The runtime selects resources for the running Python interpreter, not the OS's native architecture, and verifies their generated SHA-256 digests.

Importing a provider does not initialize Bootstrap or COM. The initializer also does not select STA/MTA; the explicit apartment-first example follows Bootstrap's documented prerequisite for compatibility across its native implementation paths. This does not imply that the existing init-first examples fail on modern Windows.

The result remains WinAppSDKContext, including resource_pri_path(). It is not a shutdown/RAII context: dropping it does not remove the native package graph or unload Bootstrap. Repeated calls retain the existing native behavior, and failed calls can be retried. Changing SDK deployment or replacing loaded binaries requires a new process.

The bundled Bootstrap DLL is not the complete Windows App Runtime. Deployment of a matching Runtime and the SDK's redistribution terms remain the application producer's responsibility. This API does not install the Runtime or silently skip initialization merely because the process has package identity. Ordinary Windows SDK-only generated bindings require none of this configuration.

Opt-in automatic Bootstrap

Import dynwinrt.auto_init on the application's chosen COM thread after preparing its apartment:

from dynwinrt import RoApartment, projected_lifetime_scope

with RoApartment(0):  # The application chooses STA here.
    import dynwinrt.auto_init

    with projected_lifetime_scope():
        # Import generated bindings and create/run the application here.
        ...

The module checks Windows package identity first, without WinRT activation. With identity it skips Bootstrap entirely: no generated configuration discovery or Bootstrap DLL lookup, read, load, or initialization. Without identity it calls the existing no-argument init_winappsdk(), so install a generated package with Bootstrap configuration as described above; configuration and native errors propagate unchanged. The skip does not verify SDK deployment or readiness.

This is opt-in: ordinary import dynwinrt and generated-package imports remain passive. Explicit init_winappsdk(...), including its no-argument form, never skips because of identity and still returns WinAppSDKContext. The automatic module exposes no context API. Normal import caching applies, not initialization on every import or a custom retry mechanism. Neither entry point initializes COM, chooses STA/MTA, creates threads/message loops, or forces an SDK reload/version switch; the application owns those lifetimes.

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 for deterministic early cleanup inside the COM apartment:

from dynwinrt import RO_INIT_SINGLETHREADED, RoApartment, projected_lifetime_scope

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

Native factory and method outputs are tracked automatically, even when a generated factory returns a bare DynWinRTValue (for example, PropertyValue.create_uint32(8080)). Independently owned COM references made inside RoApartment or after ro_initialize() are also observed weakly without an explicit scope: any still-live owners release their own references on the creating thread before the final dynwinrt-managed RoUninitialize. The raw value or generated wrapper remains a Python object, but calls after that boundary raise RuntimeError instead of releasing a native pointer in an uninitialized apartment. Earlier projected_lifetime_scope() disposal remains idempotent with apartment cleanup. It retains projected wrappers strongly, but observes raw native outputs weakly, so ordinary temporary casts still drop early. Pure scalar results do not own COM references and remain usable after either boundary. An object created under an external COM initialization alone is not automatically tied to a dynwinrt-managed apartment.

DynWinRTArray and DynWinRTStruct can independently own COM references, including after DynWinRTValue.as_array() or .as_struct() clones an already tracked value. COM-bearing containers created or extracted inside a managed apartment are observed weakly and released before its final exit, even without an explicit scope. Retained containers report is_released() and reject reads, writes and to_value() with the released-object RuntimeError, rather than exposing silently emptied storage. Scalar-only containers remain usable. Calling release() early is repeat-safe and drops only the container's own references. Cross-thread access and explicit release require the creating thread unless every contained COM reference is agile. This also applies after mutating a struct's object or nested-struct fields. An implicit foreign-thread Drop of a non-agile container quarantines its own reference with a native diagnostic rather than calling COM Release from the wrong apartment; agile containers release normally on either thread.

DynWinRTArray.from_values() and from_object_values() validate every element against its declared native type before retaining an independent reference. Array, vector, map, delegate, and method inputs first validate borrowed Python handles, then clone native COM references; a foreign-thread non-agile input cannot cause AddRef before its thread error. Mismatched scalar/object or struct identities and unsupported nested array elements raise OSError instead of storing a value that cannot be marshaled safely; nullable interface elements still accept native null. For arrays produced by lower-level native paths, lifetime tracking also checks the actual owned elements, not only the declared array element type.

Scopes nest in LIFO order. Wrappers and raw native results that survive a closed scope or final managed apartment exit remain Python objects, but their COM references are released. Using one afterwards as the object of a call, as an argument, or inside a sequence, mapping, array, or struct input raises RuntimeError explaining that it was released, as it does after release_projected(wrapper) or DynWinRTValue.release(). Returning one from an interface implementation handler fails the native call like any other handler error. DynWinRTValue.is_released() tells a released value apart from a WinRT null reference: both report is_null(), but only the null can still be passed. Each scope is thread-affine: enter, use, and close it inside that thread's RoApartment. Same-thread asyncio tasks inherit the active scope; worker threads use their own RoApartment and optionally their own earlier-cleanup scope. Native callbacks invoked on a foreign thread preserve other captured context but do not inherit the subscriber's lifetime scope. Independently cloned non-agile callback inputs created on a managed thread stay usable after an explicit scope, but are released when that thread's managed apartment finally closes. Agile callback inputs can be retained and released normally; a non-agile progress input cannot cross callback threads and reports an error instead.

An unfinished async future prevents the final apartment close without silently cancelling work. Settle or explicitly cancel it, then call the owner-thread guard's close() again. If an unnamed RoApartment context's owner cleanup failed, use retry_pending_apartment_close() on the same thread; if it was rejected during a native callback, use RoApartment.recover_pending() after the outer native call returns. A non-agile WinRT async operation awaited from asyncio checks completion on its own apartment thread rather than passing its native reference to a worker. Explicitly cancelling that asyncio task calls native IAsyncInfo::Cancel on the owner thread; ordinary apartment cleanup never cancels an external task. Calls to RoApartment.close() on another OS thread raise without changing the apartment state. An implicit wrong-thread finalizer preserves its pending native initialization for explicit recovery on the owner OS thread. If that thread has exited, or interpreter shutdown makes the Python GIL unavailable, native cleanup fails closed with a diagnostic instead of releasing COM after teardown; normal close never treats a leak as success. A final close attempted from a synchronous native callback is rejected before the owner drain and RoUninitialize, then can be retried once the callback and native call return.

Embedded host callback shutdown

If an embedded host retains native aliases to Python-backed delegates, element factories, implementations, or progress handlers, it must close their Python callback entry while Python is still alive, before Py_FinalizeEx:

  1. Stop other native threads from invoking those aliases and settle callbacks already in flight (including queued progress delivery).
  2. Call dynwinrt.shutdown_python_callbacks() on the live interpreter. This gate is idempotent; if a native callback is still active it raises RuntimeError without closing, so settle that callback and retry.
  3. Finalize Python only after the gate succeeds. Release external COM aliases according to their own ownership contract; closing an apartment never disconnects them or cancels external work.

After the gate closes, late native delegate, element-factory, XAML, and implementation entrypoints return RO_E_CLOSED without attaching to Python. Python-backed callback creation also fails explicitly. A void progress callback cannot return an HRESULT and instead issues a best-effort native diagnostic. Hosts that skip the explicit gate have only PyO3's best-effort shutdown detection: a foreign callback during the early, unobservable part of Py_FinalizeEx cannot be guaranteed deadlock-free across all supported Python versions. This external-callback limitation is separate from the managed-apartment owner-release guarantee for #189.

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.

Metadata

Release files for dynwinrt 0.1.0rc24

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for dynwinrt 0.1.0rc24
File Actions
dynwinrt-0.1.0rc24-cp314-cp314-win_arm64.whl CPython 3.14 CPython 3.14 Windows ARM64 Details for dynwinrt-0.1.0rc24-cp314-cp314-win_arm64.whl
dynwinrt-0.1.0rc24-cp314-cp314-win_amd64.whl CPython 3.14 CPython 3.14 Windows x86-64 Details for dynwinrt-0.1.0rc24-cp314-cp314-win_amd64.whl
dynwinrt-0.1.0rc24-cp313-cp313-win_arm64.whl CPython 3.13 CPython 3.13 Windows ARM64 Details for dynwinrt-0.1.0rc24-cp313-cp313-win_arm64.whl
dynwinrt-0.1.0rc24-cp313-cp313-win_amd64.whl CPython 3.13 CPython 3.13 Windows x86-64 Details for dynwinrt-0.1.0rc24-cp313-cp313-win_amd64.whl
dynwinrt-0.1.0rc24-cp312-cp312-win_arm64.whl CPython 3.12 CPython 3.12 Windows ARM64 Details for dynwinrt-0.1.0rc24-cp312-cp312-win_arm64.whl
dynwinrt-0.1.0rc24-cp312-cp312-win_amd64.whl CPython 3.12 CPython 3.12 Windows x86-64 Details for dynwinrt-0.1.0rc24-cp312-cp312-win_amd64.whl
dynwinrt-0.1.0rc24-cp311-cp311-win_arm64.whl CPython 3.11 CPython 3.11 Windows ARM64 Details for dynwinrt-0.1.0rc24-cp311-cp311-win_arm64.whl
dynwinrt-0.1.0rc24-cp311-cp311-win_amd64.whl CPython 3.11 CPython 3.11 Windows x86-64 Details for dynwinrt-0.1.0rc24-cp311-cp311-win_amd64.whl

Total release size: 7.8 MB

Release files / dynwinrt-0.1.0rc24-cp314-cp314-win_arm64.whl

Download URL dynwinrt-0.1.0rc24-cp314-cp314-win_arm64.whl
Size 950.0 kB
Tags CPython 3.14 Windows ARM64
SHA-256 checksum
How to use checksums
ee79427f4e2eeaa6d83927acfc2579627aea4a196cb9db777b400c4937c832d1
BLAKE2b-256 checksum
How to use checksums
2c09bbc7ecd97f861d4ea2b501062cc174315ba9ba735ce561a0542c82505d53
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via RestSharp/106.13.0.0

Release files / dynwinrt-0.1.0rc24-cp314-cp314-win_amd64.whl

Download URL dynwinrt-0.1.0rc24-cp314-cp314-win_amd64.whl
Size 994.7 kB
Tags CPython 3.14 Windows x86-64
SHA-256 checksum
How to use checksums
0d168d13cb046852c2ff365fbf355b6d4adba7c7050b2ca23697e3fe04314993
BLAKE2b-256 checksum
How to use checksums
1a927082106a878638faa84931cb6da9a922a649b9a8131c785c5e487cce73a9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via RestSharp/106.13.0.0

Release files / dynwinrt-0.1.0rc24-cp313-cp313-win_arm64.whl

Download URL dynwinrt-0.1.0rc24-cp313-cp313-win_arm64.whl
Size 949.4 kB
Tags CPython 3.13 Windows ARM64
SHA-256 checksum
How to use checksums
a11cf3ade01653f73a4bb77c9d6cb1fd655165c72375927634d62d5c24ceeefa
BLAKE2b-256 checksum
How to use checksums
712c55603803c965d9a20323b3179801de1c3eb915f3fbc9fba67dfb001d9b6c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via RestSharp/106.13.0.0

Release files / dynwinrt-0.1.0rc24-cp313-cp313-win_amd64.whl

Download URL dynwinrt-0.1.0rc24-cp313-cp313-win_amd64.whl
Size 994.2 kB
Tags CPython 3.13 Windows x86-64
SHA-256 checksum
How to use checksums
d5e105d579c6ba7a3a3c5a188879f874a1aeae342d55b20d248eb673b97dd1f0
BLAKE2b-256 checksum
How to use checksums
e8e01a09f4be51575227f5f81113d0900437f77cb314b87d4e1df1af6441959a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via RestSharp/106.13.0.0

Release files / dynwinrt-0.1.0rc24-cp312-cp312-win_arm64.whl

Download URL dynwinrt-0.1.0rc24-cp312-cp312-win_arm64.whl
Size 949.7 kB
Tags CPython 3.12 Windows ARM64
SHA-256 checksum
How to use checksums
e7ea1ede177ca291a405c9d3601fff956b56bf757620dce9a4df6b6dedfa6fe5
BLAKE2b-256 checksum
How to use checksums
fabd402569a7183e28b0757ac6e1c2de64a86e6bf43f70c80d53f5d7cc2525ca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via RestSharp/106.13.0.0

Release files / dynwinrt-0.1.0rc24-cp312-cp312-win_amd64.whl

Download URL dynwinrt-0.1.0rc24-cp312-cp312-win_amd64.whl
Size 994.3 kB
Tags CPython 3.12 Windows x86-64
SHA-256 checksum
How to use checksums
0fa3ace86cce881fc302f3d2c1ff1ab3caca5980b288ca191adf6adb6da18150
BLAKE2b-256 checksum
How to use checksums
38de9a88730b4ed4354d9389c795eac7780f64e8894748c4693434ab54d3e836
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via RestSharp/106.13.0.0

Release files / dynwinrt-0.1.0rc24-cp311-cp311-win_arm64.whl

Download URL dynwinrt-0.1.0rc24-cp311-cp311-win_arm64.whl
Size 961.3 kB
Tags CPython 3.11 Windows ARM64
SHA-256 checksum
How to use checksums
f133b36caf6ae77bb20769ca758a447e380f230e7bbb54f9e3aa23c85704fe9b
BLAKE2b-256 checksum
How to use checksums
3b973d6ea402ae59d9c087491127476aa35a561a6bc8f8e276086b1a90fd8332
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via RestSharp/106.13.0.0

Release files / dynwinrt-0.1.0rc24-cp311-cp311-win_amd64.whl

Download URL dynwinrt-0.1.0rc24-cp311-cp311-win_amd64.whl
Size 1.0 MB
Tags CPython 3.11 Windows x86-64
SHA-256 checksum
How to use checksums
1fbcadcceba7387bd07c919638d2de616a8e51e2e653deea85d640f540d64fc1
BLAKE2b-256 checksum
How to use checksums
294bf138247f91fdde15e67c2664c468de76873903ac2a30aad258a18c164968
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via RestSharp/106.13.0.0

Security for dynwinrt 0.1.0rc24

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page