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 asT | Noneeverywhere;- results of
Try*members, such astry_get_item_async()orJsonObject.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()orStorageFolder.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/IInspectablevalues (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,boolandstr; GUIDs useuuid.UUID; DateTimeuses a timezone-aware UTCdatetimeandTimeSpanatimedelta, truncated to microseconds like other positions; aDateTimebeyonddatetime.maxraisesOverflowError;Point,SizeandRectuse the immutabledynwinrt.valuestypes;UInt8Arrayusesbytes, other arrays use lists, andInspectableArrayelements 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:
Nonebecomes WinRT null; aDynWinRTValueholding an object is returned unchanged, and a projected wrapper returns its nativeDynWinRTValue;bool,float,str, a timezone-awaredatetime,timedelta,uuid.UUID/WinGUID,bytes/bytearray/memoryview, anddynwinrt.valuestags, typed arrays andPoint/Size/Rectbox as their exact type; a naivedatetimeraisesValueError;- a plain
intboxes asInt32only; other values raiseOverflowError; - a list or tuple boxes only when all elements have the same unambiguous
type, for example all
str(StringArray) or all in-range plainint(Int32Array); empty and mixed lists raiseTypeError; - enum members raise
TypeErrorunless an integerproperty_typeis given. Boxing a WinRT enum asIReference<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
IMapgets aMutableObjectValueView, acollections.abc.MutableMapping; anIMapViewgets a read-onlyObjectValueView, aMapping. 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 asNone. - Reads return
Nonefor WinRT null, a runtime object that is not a box as itsDynWinRTValue, andunbox_object(value, preserve_type=preserve_type)for a box. Whereunbox_objectraises for a box without a Python form, the view returns the box'sDynWinRTValue, sodict(view)does not fail on one odd entry: an unsupportedPropertyTypesuch asOtherTypeanywhere inside the box, or aDateTimeoutside the range ofdatetime. Other errors propagate, including the same HRESULT from a failing getter for an otherwise supportedPropertyType. Each read returns a newDynWinRTValuefor a runtime object, so compare those withidentity_raw(). - Writes store
to_winrt_object(value)and raise its errors unchanged: a plainintboxes asInt32only, and an empty or mixed list needs a typed array or an explicitproperty_type. view.rawis the generated map, which keeps returning nativeDynWinRTValueobjects. Use it to keep a box's COM identity: withpreserve_type=True, a value that is read and written back keeps itsPropertyType, but it is a new box.- The generated wrapper declares the exact
IMap/IMapViewinterface used by its mapping methods, andQueryInterfaceconfirms 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 asStringMaporJsonObject, raiseTypeError; the type stubs reject them too. Runtime-class wrappers generated without this declaration fail closed; reproject them withvalue.as_interface(IMap_String_Object)(or the Guid/read-only equivalent). - Generated
IPropertySetwrappers, such asApplicationDataContainer.values, are not Python mappings. Passcontainer.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 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.
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:
- Stop other native threads from invoking those aliases and settle callbacks already in flight (including queued progress delivery).
- Call
dynwinrt.shutdown_python_callbacks()on the live interpreter. This gate is idempotent; if a native callback is still active it raisesRuntimeErrorwithout closing, so settle that callback and retry. - 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.
- 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.
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)
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.