The WinRT type system¶
PyWinRT is a "projection" that uses the Windows Runtime (WinRT) type system to automatically generate Python bindings for the Windows SDK. This page describes how the WinRT types look and feel in this Python projection.
Naming conventions¶
In the WinRT documentation, most names use PascalCasing in the style of .NET.
In the Python projection, names are adapted to fit PEP8 naming conventions.
- Type names remain as
CapitalizedWords. - Constants, such as enum members are converted to
UPPER_CASE_WITH_UNDERSCORES. - Namespaces are converted to
lowercasewithoutunderscores. - All other identifiers are converted to
lower_case_with_underscores(fields, properties, methods, events, etc.). - If an identifier is a Python keyword, an underscore is appended to the name.
- A protected or overridable member, which WinRT means for a class derived
from its class, starts with an underscore, as in
_get_template_child()and_default_style_key.
Distribution packages¶
Each Windows SDK namespace is projected as a separate Python package. The packages are
named like winrt-Windows.Foundation
where winrt is the root namespace for a specific SDK or DLL and
Windows.Foundation is a WinRT namespace within that SDK or DLL.
Installing says how the other APIs are packaged.
There are also some PyWinRT-specific sub-packages distributed in the winrt-runtime package. As well as some extra interop packages that bridge between WinRT and Win32 types.
See also
Namespaces¶
Each Windows SDK namespace is projected as a Python package and follows the usual Python import conventions:
# import members of a namespace
from winrt.windows.foundation import Uri
# or import the namespace module itself
import winrt.windows.foundation as wf
As with the distribution package names, the WinRT namespace names have a root
namespace that matches the part of the package name before the - followed
by the namespace. The modules names are lowercasewithoutunderscores, so
namespaces with multiple words in one segment, like Windows.AI.MachineLearning
are projected as winrt.windows.ai.machinelearning.
Fundamental types¶
The WinRT type system defines the following fundamental types which are mapped to the corresponding Python builtin types.
| WinRT type | Python type | Python format string | Description |
|---|---|---|---|
Boolean |
bool |
"?" |
an 8-bit Boolean value |
Int8 |
int |
"b" |
an 8-bit signed integer |
Int16 |
int |
"h" |
a 16-bit signed integer |
Int32 |
int |
"i" |
a 32-bit signed integer |
Int64 |
int |
"q" |
a 64-bit signed integer |
UInt8 |
int |
"B" |
an 8-bit unsigned integer |
UInt16 |
int |
"H" |
a 16-bit unsigned integer |
UInt32 |
int |
"I" |
a 32-bit unsigned integer |
UInt64 |
int |
"Q" |
a 64-bit unsigned integer |
Single |
float |
"f" |
a 32-bit IEEE 754 floating point number |
Double |
float |
"d" |
a 64-bit IEEE 754 floating point number |
Char16 |
str 1 |
"u" 2 |
a 16-bit non-numeric value representing a UTF-16 code unit |
String |
str |
"P" |
an immutable sequence of Char16 used to represent text |
Guid |
uuid.UUID |
"T{I2H8B}" 3 |
a 128-bit standard globally unique identifier |
The Python format strings use the syntax defined in the struct module
and the PEP 3118 additions and are used as the format for the
buffer protocol for arrays of these types. These format strings can be
read at runtime by using memoryview.format.
An integer type takes an int or any object with an __index__() method,
such as a NumPy integer, as Python's own functions do, and the type hints of a
parameter say so with typing.SupportsIndex, as in
winrt.system.UInt32 | typing.SupportsIndex. A number out of its range raises
OverflowError, except that a negative number for an unsigned
type raises ValueError.
Changed in version 4.0
A negative number for an unsigned type raises ValueError, as Python
3.14's PyLong_AsUInt32() and PyLong_AsUInt64() do. Previously it raised
OverflowError.
A Char16 takes a str of one character. A character outside the Basic
Multilingual Plane, such as an emoji, needs two UTF-16 code units and raises
ValueError.
Changed in version 4.0
A character outside the Basic Multilingual Plane raises ValueError.
Previously it raised TypeError, saying the string was of length 2.
The eleven types that have no Python type of their own are named in Python by
the aliases in winrt.system, such as winrt.system.Int32. Each
alias is annotated with what its type is called in each of the three languages
involved - winrt.system.BufferFormat,
winrt.system.StructFormat
and winrt.system.WinrtSignature - so a program
that needs one of them finds it with an isinstance() check over
typing.get_args().
Enums¶
WinRT enums are projected using the Python standard library
enum module. The
WinRT Type system has a [Flags] attribute that indicates if an enum is
treated as bit flags or not. Enum types without this attribute are projected as
an enum.IntEnum type
or if the [Flags] attribute is present, the type is
projected as an enum.IntFlag type.
A newer version of Windows can return a value that the installed package
was generated without. Such a value is still an instance of the enum: it
equals its number and is named with it, as in "99", but it is not one of the
enum's members, so it is not listed when the enum is iterated. Calling an enum
with a number it has no member for gives the same kind of value rather than a
ValueError. A flags enum keeps a bit it has no member for in the same way.
An array of enums is named with the enum type itself, as in
Array(SomeEnum, 3). A buffer passed where such an array is expected uses
the "i" format string for a regular enum and "I" for a flags enum.
Structs¶
Structs are simple data types that are passed by value in WinRT. In the Python
projection, each struct is wrapped in a Python object that is similar to a
typing.NamedTuple
or frozen dataclasses.
The names of fields are converted to use the lower_case_with_underscores
naming convention.
Projected structs are @typing.final
classes, so they cannot be subclassed. Projected structs are immutable. To modify a
struct, use copy.replace() which creates a modified copy.
Because a struct is immutable, copy.copy() and
copy.deepcopy() return the struct itself, as they do for a
tuple, so copying a list or a dataclass that holds structs works. A struct
cannot be pickled.
As a convenience, plain tuple
objects can be used in place of projected
structs when calling methods that expect a projected struct or when setting
properties that expect a projected struct.
Each projected struct also has an unpack() method that converts it to a
tuple.
Example:
# window.position is a winrt.windows.graphics.PointInt32
# fails with AttributeError because Point is immutable
window.position.x = 200
# modifying
new = copy.replace(window.position, x=200)
# passing a tuple
window.move_and_resize((100, 100, 800, 600))
# unpacking
x, y = window.position.unpack()
Changed in version 2.2
Changed __repr__ implementation to give a representation that can be
passed to eval().
Changed in version 3.0
- Structs are now immutable. In previous versions, attributes could be set.
- Added
__replace__method to allow for use withcopy.replace(). - Added
unpack()method to convert to a tuple. Added support for using plain tuples in place of projected structs as method arguments.
Objects¶
A WinRT runtime class is projected as a Python class whose instances wrap
WinRT objects. Every one derives from winrt.system.Object.
Calling the class constructs an object with the class's activation factory.
A class with several constructors chooses one by the number of arguments, in
the same way as methods, and the arguments are positional only. A
class that has no constructor raises TypeError when it is
called, and a type checker refuses the call. Its objects come from the methods
and properties of other objects.
from winrt.windows.foundation import Uri
uri = Uri("https://example.com/")
page = Uri("https://example.com/", "docs/index.html")
Static members - methods, properties and events - are reached through the class rather than an instance.
A runtime class that WinRT declares composable can be subclassed in Python,
and the subclass can override the class's overridable methods, which have a
leading underscore like the other protected members. Any other class refuses
to be a base class with a TypeError.
from winrt.windows.ui.xaml import Application
class App(Application):
def _on_launched(self, args) -> None:
...
Besides the class it derives from, such a subclass can have only WinRT
interfaces as bases, which it then implements, as an application that is a
XAML metadata provider adds IXamlMetadataProvider. Any other base, such as a
Python mixin or a second runtime class, raises TypeError.
Inheritance is linear, as in C#. A subclass can derive from another Python
subclass of its class, but not from two of them, and not from a Python subclass
of a class its own class derives from, such as one written for Control reused
under ContentControl. Nor can it be generic, since a WinRT class has no type
parameters, so typing.Generic is refused like any other Python base.
A class whose constructor is protected, such as Panel or Control, is
created only through a class derived from it, as in C#, so calling the class
itself raises TypeError. A type checker cannot tell the two
calls apart and accepts both.
from typing_extensions import override
from winrt.windows.ui.xaml.controls import Panel
class StackingPanel(Panel):
@override
def _measure_override(self, available_size):
...
panel = StackingPanel()
Added in version 4.0
Deriving from a class whose constructor is protected.
A subclass with an __init__ of its own takes the arguments that __init__
declares, keywords included, as any Python class does. Its WinRT object is
made when __init__ calls super().__init__(), whose arguments choose the
constructor, and until then the object has no WinRT members to use: using one
raises RuntimeError. A subclass without an __init__ passes
the arguments of the call to the constructor, and so does one whose __init__
returns without calling super().__init__(), once it has returned: an
__init__ that only keeps Python state of its own needs no call to the base
class.
class TimelinePanel(Panel):
def __init__(self, min_column_width=180, spacing=10):
super().__init__()
self.min_column_width = min_column_width
self.spacing = spacing
panel = TimelinePanel(spacing=12)
Changed in version 4.0.2
A subclass's __init__ takes the arguments it declares, and its object has
no WinRT members until it calls super().__init__(). Previously every
argument of the call went to the constructor before __init__ ran.
WinRT asks such an object for its runtime class name, which by default is the
name of the first overridable interface its class implements, or nothing when
the class has none. A XAML metadata provider looks types up by that name, so a
subclass can give its own with the runtime_class_name keyword, and a class
derived from it inherits that name:
from winrt.windows.ui.xaml.controls import Page
class MainPage(Page, runtime_class_name="MyApp.MainPage"):
...
A wrapper is not the WinRT object itself, and reading the same object twice,
for example from a property, gives two wrappers. They compare equal with ==
and have the same hash(), because those ask whether the two hold the same
WinRT object, but is tells them apart. A mapping is the
exception: it compares by content and has no hash, as a dict does. Two
objects that only hold equal values, such as two Uri objects of the same
address, are not equal. An object that implements IStringable converts to
its string with str(), and repr() is Python's default.
A wrapper cannot be weakly referenced: the same WinRT object can come back as another wrapper, so a weak reference to one wrapper would die while the object lives on. A Python class derived from a projected class can be weakly referenced, and so can a Python implementation of an interface.
A null object comes back as None. The type hints annotate a method or
property | None where WinRT's documentation says it can return null, but
they do not yet cover every namespace, so a member without the annotation can
still return None where WinRT returns null.
An object that has resources to release implements IClosable and is a
context manager.
Query interface¶
A WinRT object implements several interfaces, and the type that a method or
property hands it back as is only the one its signature declares. When the
signature says Object, for example, the wrapper has none of the members of
the object's class. as_() asks the object for another interface, which WinRT
calls QueryInterface, and returns a wrapper of that type:
from winrt.windows.foundation import Uri
from winrt.windows.foundation.collections import PropertySet
properties = PropertySet()
properties.insert("home", Uri("https://example.com/"))
home = properties.lookup("home") # a winrt.system.Object
print(home.as_(Uri).host)
as_() takes a runtime class or an interface, and raises OSError
when the object does not implement it. Given anything else, a parameterized
interface such as IVector[str] included, it raises TypeError.
isinstance() asks the same question without raising; see
Interfaces.
Changed in version 4.0
Type._from(obj), the spelling from before as_(), is deprecated and
raises a DeprecationWarning. Write obj.as_(Type)
instead.
See also
winrt.runtime.interop.as_interface() for an
interface that the projection does not know.
Methods¶
Methods are named with the lower_case_with_underscores
naming convention. Their arguments are positional only,
and a keyword argument raises TypeError.
A method with several outputs returns them as a tuple: the return value first, then the output parameters in the order they are declared.
WinRT supports method overloading - several methods of one type with the same
name. Overloads usually differ in the number of parameters, and a call chooses
the overload that takes as many arguments as it was given. Too many or too few
arguments raise TypeError, as do arguments of the wrong type.
folder.create_file_async("spam.txt")
folder.create_file_async("spam.txt", CreationCollisionOption.REPLACE_EXISTING)
Some types have several overloads that take the same number of parameters. Those cannot be told apart by the number of arguments, so each one is projected with the unique name it has in the WinRT metadata. One of them still keeps the shared name: an overload that has no unique name in the metadata, otherwise the one that the metadata marks as the default overload.
# FindAllAsync(DeviceClass), the default overload
DeviceInformation.find_all_async(DeviceClass.AUDIO_CAPTURE)
# FindAllAsync(String)
DeviceInformation.find_all_async_aqs_filter(aqs_filter)
Overridable methods - the methods that a Python subclass of a composable class implements - are the exception to the rule. Python methods cannot be overloaded, so each overload of an overridable method keeps its own unique name from the metadata.
Changed in version 4.0
PyWinRT 3.x used the unique name for every overload that had one, even
when the overload could have been chosen by the number of arguments. For
the methods that are not overridable, the Windows SDK packages keep those
names as aliases that raise a DeprecationWarning, and they will be
removed under the deprecation policy.
The other packages do not have them. scripts/3to4/inspect_source.py in
the repository finds them in your code.
Properties¶
Properties are projected as Python descriptor-like attributes. Properties with WinRT getter support getting and properties with a WinRT setter allow setting. Deleting properties is never allowed.
Names of properties are converted to use the lower_case_with_underscores
naming convention.
Example:
Static properties are implemented as class attributes via a metaclass, so are accessed by using the type object rather than an instance object:
Changed in version v1.0.0b8
Previous beta releases implemented static properties as get_name() and
put_name() static methods instead of class attributes.
Events¶
An event is projected as a pair of methods, add_<event>() and
remove_<event>(), with the event name converted to the
lower_case_with_underscores naming convention.
add_<event>() takes a handler and returns an EventRegistrationToken;
passing that token to remove_<event>() removes the handler again. An event
can have any number of handlers, and each add_<event>() call registers one
more. The token is a subclass of int, so it can be compared, hashed
and used as a dictionary key.
The handler is a callable that takes two positional arguments, the object that raised the event and an event arguments object, and returns nothing. The types of both are in the stubs as the parameters of the event's delegate type.
from winrt.windows.devices.bluetooth.advertisement import (
BluetoothLEAdvertisementReceivedEventArgs,
BluetoothLEAdvertisementWatcher,
)
def on_received(
sender: BluetoothLEAdvertisementWatcher,
args: BluetoothLEAdvertisementReceivedEventArgs,
) -> None:
print(args.bluetooth_address)
watcher = BluetoothLEAdvertisementWatcher()
token = watcher.add_received(on_received)
watcher.start()
...
watcher.stop()
watcher.remove_received(token)
Static events are added and removed on the type object rather than on an instance, in the same way as static properties:
from winrt.windows.applicationmodel.core import CoreApplication
token = CoreApplication.add_unhandled_error_detected(on_error)
Which thread calls the handler¶
WinRT calls the handler on whatever thread raises the event, and the handler runs as ordinary Python code on that thread. For most objects that is a WinRT thread pool thread, not one of the program's own. In a GUI app, an object that lives on the UI thread raises its events there, so a XAML event handler runs on the UI thread.
This means a handler must not touch anything that belongs to a particular
thread. With asyncio, hand the work to the event loop with
loop.call_soon_threadsafe() rather than doing it in
the handler, and use an asyncio.Queue or an
asyncio.Future to deliver the event arguments to a
coroutine:
import asyncio
from winrt.windows.devices.bluetooth.advertisement import BluetoothLEAdvertisementWatcher
async def scan() -> None:
loop = asyncio.get_running_loop()
received = asyncio.Queue()
watcher = BluetoothLEAdvertisementWatcher()
token = watcher.add_received(
lambda sender, args: loop.call_soon_threadsafe(received.put_nowait, args)
)
try:
watcher.start()
while True:
args = await received.get()
print(args.bluetooth_address)
finally:
watcher.stop()
watcher.remove_received(token)
A handler is a plain function, not an async def one: WinRT does not await
what a handler returns, so the body of an async def handler would never run,
and passing one raises TypeError. None raises TypeError too, since it is
no handler. A handler is taken off with remove_<event>().
The sender carries on after the handler returns, so a property of the sender read later from a task on the event loop describes the object as it is then, not as it was when the event was raised. Where that matters, read the property in the handler and hand the value to the event loop instead of the object.
Some events are designed for the handler to finish its work later: their
event arguments have a get_deferral() method. The object that raised the
event waits until the deferral is completed, so a handler can take the
deferral, schedule a coroutine on the event loop, and have that coroutine call
complete() on the deferral when it is done. Everything else the handler
needs must still be read before the handler returns.
def on_suspending(sender, args):
deferral = args.suspending_operation.get_deferral()
async def save() -> None:
try:
await save_state()
finally:
deferral.complete()
asyncio.run_coroutine_threadsafe(save(), loop)
Removing handlers¶
The WinRT object keeps a reference to the handler until it is removed. A
handler that is a closure or a bound method keeps its own object alive too, so
an object that subscribes to an event on a WinRT object it owns will not be
garbage collected until the handler is removed, however unreachable the pair
become. Keep the token and remove the handler when the object is done with the
event. contextlib.ExitStack pairs an add_<event>() with its
remove_<event>() at the point of subscribing:
import contextlib
with contextlib.ExitStack() as stack:
token = frame_pool.add_frame_arrived(on_frame)
stack.callback(frame_pool.remove_frame_arrived, token)
...
remove_<event>() stops the handler from being called again, but it does not
wait for a call that is already in progress on another thread. A handler can
still be running when remove_<event>() returns.
Removing a handler is not required before the interpreter exits. An event that is raised while Python is shutting down, or after it has shut down, is dropped rather than delivered.
Exceptions in handlers¶
An exception that escapes a handler goes to sys.unraisablehook(), as for any
callback WinRT calls, so handle exceptions inside the handler. See
Exceptions.
Interfaces¶
A WinRT interface is projected as an abstract Python class. It cannot be instantiated, since an interface is something an object implements and not an object of its own.
A projected class does not derive from the interfaces it implements in Python,
but isinstance() and issubclass() answer for
them anyway. isinstance(obj, IStringable) asks the object, in the same way as
as_(), and issubclass(Uri, IStringable) is answered
from what the projection says the class implements, including the interfaces
it has through a base class and the interfaces those interfaces require.
from winrt.windows.foundation import IStringable, Uri
uri = Uri("https://example.com/")
assert isinstance(uri, IStringable)
assert issubclass(Uri, IStringable)
A parameterized interface, such as IVector[str], is for type hints.
isinstance() and issubclass() raise TypeError for it, with
or without type arguments, since without them there is no interface to ask
for and with them Python refuses any parameterized generic type.
An object whose declared type is an interface is an instance of that
interface. Its exact type, which type() and repr() show with a leading
underscore, such as _IStringable, is an implementation detail that may
change, so check for the interface with isinstance(obj, IStringable) rather
than comparing types.
Implementing an interface¶
Passing a WinRT API an object of your own is rarely needed. Where a parameter
is a collection interface, such as IIterable or IMap, pass a Python
list, dict or other collection instead. For any other
interface, use one of the runtime classes that implement it. A parameterized
interface, such as IVector[str], cannot be implemented in Python at all:
deriving a class from one raises TypeError.
Some APIs call back into an object that the program provides, as XAML does with a value converter. For those, a Python class that derives from an interface implements it, and its instances can be passed wherever WinRT wants that interface. The class defines the interface's methods and properties, and those of every interface the interface requires, and WinRT sees one object that answers to all of them. The type hints mark the members abstract, so a type checker reports one that is missing; at run time a missing member fails only when WinRT calls it.
The class cannot also derive from an abstract base class, such as one
from collections.abc: Python reports a metaclass conflict, because the
metaclass of a WinRT type is not ABCMeta, although the type hints
declare it one so that a type checker enforces the abstract members.
The type hints of a member say what a caller may pass, which is more than the
WinRT type: a tuple for a struct, a Python collection for a collection
interface, any buffer for an IBuffer, any object with __index__() for an
integer. A method of an implementation can be called from Python as well as by
WinRT, and only a call from WinRT is converted, so it takes the same types, as
target_type does below. operator.index() turns such an
integer into an int, and a type checker takes the parameter as an int after
count = operator.index(count).
from typing_extensions import override
from winrt.system import Object, box_string, unbox_string
from winrt.windows.ui.xaml.data import IValueConverter
from winrt.windows.ui.xaml.interop import TypeKind, TypeName
class UpperCase(IValueConverter):
@override
def convert(
self,
value: Object,
target_type: TypeName | tuple[str, TypeKind],
parameter: Object,
language: str,
) -> Object:
return box_string(unbox_string(value).upper())
@override
def convert_back(
self,
value: Object,
target_type: TypeName | tuple[str, TypeKind],
parameter: Object,
language: str,
) -> Object:
return value
Passed to WinRT and handed back again, for example by storing it in a
collection and reading it back, the object comes back as the same Python
object rather than as a wrapper. It has the members of
Object as a wrapper does, so as_() of an interface
its class implements is the object itself.
WinRT calls the methods on whatever thread it calls them from, as it does
event handlers. An exception that escapes
one goes to sys.unraisablehook(), as for any callback WinRT calls. See
Exceptions.
Changed in version 4.0
An interface refuses to be instantiated, and issubclass() answers for
the interfaces a projected class implements, where it used to answer
False. isinstance() and issubclass() raise TypeError for a
parameterized interface without type arguments.
Delegates¶
A WinRT delegate is a callback. Where a method, a property or an event wants
one, pass any Python callable that takes the delegate's parameters
positionally. The type hints name the delegate type as an alias of
Callable that spells out those parameters and the result, so
there is no class to construct: pass the function itself.
WinRT calls the callable and does not await what it returns, so an
async def function raises TypeError wherever a delegate is
passed, since its body would never run. None passes no delegate, where WinRT
accepts that, except as an event handler or as an async operation's
completed or progress handler, which raise TypeError for it.
Changed in version 4.0
An async def function, and None where it is refused, raise
TypeError. Previously the body of an async def function never ran,
and None could crash the process.
A delegate that has outputs returns them in the same way as a method: its return value alone, or, with output parameters, a tuple of the return value followed by the output parameters in the order they are declared. A result of the wrong shape is an error, as an exception would be, and the caller gets the error code described below.
WinRT keeps the callable alive for as long as it holds the delegate. It calls the callable on whatever thread it calls the delegate from, which is often not one of the program's own threads; see Which thread calls the handler.
A delegate that WinRT hands to Python, such as the value of an async
operation's completed property, is callable and calls the delegate. It is a
wrapper, not the Python callable that may have been set there.
Arrays in delegates¶
A delegate's array parameters arrive as winrt.system.Array
objects, which WinRT has three uses for:
- an array the caller passes arrives as an
Arrayof its own, which the callable may keep; - an array the caller lends to be filled arrives as an
Arraywhose elements are the caller's. The callable assigns them by index,lent[i] = value, and cannot export the array as a buffer, somemoryview(lent),bytes(lent)and passing it on to another WinRT call raiseBufferError. Once the call returns, the array is empty, since its elements went back to the caller; - an array the callable returns, as its result or as an output parameter, is
an
Arrayof the declared element type, or for a fundamental type or a struct of fundamental types, any buffer of the same layout.
Exceptions in delegates¶
An exception that escapes the callable goes to sys.unraisablehook(), as for
any callback WinRT calls, and a delegate that returns a value then returns its
caller only the error. Handle exceptions inside the callable. See
Exceptions.
Arrays¶
WinRT arrays are projected as winrt.system.Array, a
fixed-size Sequence whose items can be assigned. It is not a
MutableSequence: its length never changes, so it has no
append(), insert() or del.
from winrt.system import Array, Int32
from winrt.windows.foundation import Uri
numbers = Array(Int32, 3) # three zeros
numbers = Array(Int32, [1, 2, 3]) # from an iterable
uris = Array(Uri, [Uri("https://example.com/")])
numbers[0] = 4
numbers[-1] = 6
The first argument is the element type: a projected type, the Python type that
a WinRT type is projected as, such as str or bool, or for a fundamental
type that has no Python type of its own, one of the
winrt.system aliases such as Int32. The second is a length,
an iterable of elements, or, for an element type that is a fundamental type or
a struct of fundamental types, a buffer of the same layout to copy.
Indexing counts from the end for a negative index and raises
IndexError out of range. A slice is a new Array with copies
of the elements; a slice with a step is not supported and raises
NotImplementedError. Two arrays are equal when their
elements are, whatever their element types, and an Array equals only another
Array, as a list equals only a list. An array is not hashable, and its
repr() names the element type and the elements:
Arrays as buffers¶
An Array supports the buffer protocol, in the format given for its element
type in the table above, or for one of the
numerics structs as a block of floats. An array of fundamental types
or of structs of fundamental types exports a writable buffer, so
memoryview, struct and similar can read and write
its elements in place. An array whose elements hold references - strings,
objects, or structs with a string in them - exports a read-only buffer of the
references themselves, and while such a buffer is exported, assigning an
element raises BufferError.
Arrays as parameters¶
A method's array parameters take three forms:
- an array passed to WinRT to read takes an
Arrayof the declared element type, or for a fundamental type or a struct of fundamental types, any buffer of the same layout, such as anarray.arrayor abytesobject; - an array that WinRT fills takes an
Arrayof the right length, or a writable buffer, and WinRT writes its elements; - an array that WinRT returns comes back as an
Array.
from winrt.windows.security.cryptography import CryptographicBuffer
buffer = CryptographicBuffer.create_from_byte_array(b"\x01\x02\x03")
An array of references, such as strings, has no buffer that says what it
points at, so only an Array is accepted for one.
Arrays that WinRT passes to a delegate are described under Arrays in delegates.
See also
winrt.system.Array for its signature and what
changed in version 4.0.
Exceptions¶
A WinRT call that fails raises OSError with
OSError.winerror set to the HRESULT error code
it failed with.
Python picks the class from the HRESULT, as it does for a failed Win32 call. One
that carries a Win32 error code, of the form 0x8007xxxx, may raise a subclass
of OSError: a path that does not exist raises
FileNotFoundError, and E_ACCESSDENIED raises
PermissionError. Catching OSError catches them all, and
winerror is the HRESULT either way.
See also
winrt.system.hresult for some common error codes.
An error code that WinRT hands back as a value rather than raising, such as an
async operation's error_code, is an HResult.
On the other hand, Python exceptions cannot be propagated to C++ code. WinRT
requires that errors are serializable, but Python exceptions are not. So if a
Python callback from C++ code (i.e. an event handler or other delegate or a
method of a Python subclass of a WinRT interface) raises an exception that isn't
handled before the method returns, it will trigger the
sys.unraisablehook()
handler in Python and cause the C++ code to receive an HRESULT error code
of PYWINRT_E_UNRAISABLE_PYTHON_EXCEPTION.
This can cause undefined behavior in the C++ code in some cases, so it should
be avoided.
A Python list, dict or other collection passed where WinRT takes a
collection is read through such callbacks too, so an item that fails to
convert, such as an int in a list passed for an IVector<String>, is
reported to sys.unraisablehook() in the same way, and the call it was passed
to fails with that error code. An index or key the collection does not have
is the one failure WinRT has a name for, and gets E_BOUNDS instead.
Specialized types¶
Some types have special handling and don't strictly follow the patterns described above.
Awaitables¶
WinRT has many _async methods that perform background operations and
return a type that derives from Windows.Foundation.IAsyncInfo to be able to
wait for the result. There are four fundamental types that derive from this:
- IAsyncAction - represents an operation that does not return a value
- IAsyncOperation<TResult>
- represents an operation that returns a value of type
TResult - IAsyncOperationWithProgress<TResult, TProgress>
- represents an operation that returns a value of type
TResultand reports progress of typeTProgress - IAsyncActionWithProgress<U>
- represents an operation that does not return a value and reports progress
of type
TProgress
Synchronous usage¶
Without asyncio, a thread can block until an operation has finished with
get() or wait().
get¶
IAsyncAction.get(self) -> None
IAsyncOperation.get(self) -> TResult
IAsyncActionWithProgress.get(self) -> None
IAsyncOperationWithProgress.get(self) -> TResult
These methods block until the operation is complete.
Returns: The result of the operation.
Raises:
OSErrorif the operation failed or was canceled.RuntimeErrorif called from a single-threaded apartment (i.e. a GUI thread).
Warning
This method can't be interrupted by Ctrl+C which means
that a KeyboardInterrupt will not be raised until the operation
is complete.
Added in version 3.2
wait¶
IAsyncAction.wait(self, timeout: float, /) -> AsyncStatus
IAsyncOperation.wait(self, timeout: float, /) -> AsyncStatus
IAsyncActionWithProgress.wait(self, timeout: float, /) -> AsyncStatus
IAsyncOperationWithProgress.wait(self, timeout: float, /) -> AsyncStatus
These methods block until the operation is complete or the timeout is reached, whichever comes first.
| Parameter | Type | Description |
|---|---|---|
timeout |
float |
The timeout in seconds. A timeout that is not a positive number of seconds returns the status as it stands, without waiting. If the operation is still running when the call returns, whether the timeout ran out or was not positive, the wait keeps the one completed handler the operation allows, so awaiting it or calling get() or wait() again raises OSError until it has finished (see Asynchronous usage). To look without waiting, read the operation's status instead. |
Returns: The status of the operation. In case of a timeout, the status will be
AsyncStatus.STARTED.
Raises:
RuntimeErrorif called from a single-threaded apartment (i.e. a GUI thread).
Warning
This method can't be interrupted by Ctrl+C, which means
a KeyboardInterrupt will not be raised until the operation is
complete or the timeout is reached.
Added in version 3.2
Asynchronous usage¶
Note
WinRT async methods look like Python coroutines (methods defined with
async def) but they are not. This means they do not return a
Coroutine
object and therefore cannot be passed to
asyncio.create_task()
or TaskGroup.create_task(). They are only Awaitable
objects, so await one in a coroutine of your own and make the task from
that.
If you are using asyncio, then you can use the await keyword to wait
for the result of async WinRT methods:
To put a time limit on operations, use
asyncio.timeout(),
and to run several at once, an
asyncio.TaskGroup
with a coroutine for each:
from winrt.windows.storage import FileIO
async def read(file) -> str:
return await FileIO.read_text_async(file)
async with asyncio.timeout(10):
async with asyncio.TaskGroup() as group:
first = group.create_task(read(file1))
second = group.create_task(read(file2))
print(first.result(), second.result())
Where asyncio needs a future, such as in
asyncio.wait(),
it makes a task around the operation. Make the tasks yourself with
asyncio.ensure_future()
to match what it hands back with the operations.
An operation that has finished can be awaited again, and gives the same result
or fails in the same way each time. Awaiting, the blocking get() and
wait(), and the completed property are three ways of being told that an
operation has finished, and WinRT accepts only one completed handler per
operation, so use one of them for each operation that is still running. To
wait for one in several places, await a task made with ensure_future().
Cancellation¶
A task that is canceled while it awaits an operation, by asyncio.timeout() or
by a TaskGroup whose other task failed, for example, asks WinRT to cancel the
operation and stays suspended until the operation has finished, so no canceled
operation is left running behind a task that has moved on. Then it raises
asyncio.CancelledError,
which asyncio.timeout() turns into TimeoutError, even if the operation was
too far along to stop and finished anyway.
Canceling the task again while it waits for that gives up waiting: the task ends at once and the operation is left to finish by itself. That is the way out of an operation that never answers the request. If the operation fails after that, its error goes to the event loop's exception handler, as an error that nothing retrieved from a future does.
asyncio.run()
cancels the tasks its coroutine leaves running and waits for them, so it does
not return until every operation they await has finished. An operation that
keeps running after the request holds it up for as long as it runs. Cancel such
a task a second time before the coroutine returns to leave the operation
behind.
A task that is canceled before it has started never awaits its operation, so the operation is not asked to stop.
The operation's own cancel() is IAsyncInfo.Cancel(), which asks WinRT to
cancel it and returns nothing. An await of an operation canceled that way
raises OSError with winerror set to
WIN32_ERROR_CANCELLED, as it does
for one that Windows canceled, because a device went away, for example.
Changed in version 4.0
An operation that has finished can be awaited again. Previously a second
await failed. An error that an operation reports after the task awaiting
it gave up waiting goes to the event loop's exception handler.
Changed in version 3.2
If the Awaitable that wraps the operation is
canceled, it will now propagate the cancellation to the WinRT async
action/operation. To restore the previous behavior, you can wrap the
operation in asyncio.shield().
Other usage¶
Without asyncio, and on a thread that refuses get() and wait(), such as a
GUI thread, set the operation's completed property to a
delegate. WinRT calls it with the operation and its AsyncStatus
once the operation has finished, and in it get_results() gives the result
without blocking. It raises the error of an operation that failed, and
OSError for one that was canceled:
from winrt.windows.foundation import AsyncStatus
from winrt.windows.storage import StorageFolder
def on_completed(operation, status: AsyncStatus) -> None:
if status == AsyncStatus.CANCELED:
return
try:
folder = operation.get_results()
except OSError as e:
print("failed:", e)
else:
print("opened", folder.name)
operation = StorageFolder.get_folder_from_path_async(r"C:\Windows")
operation.completed = on_completed
WinRT calls the handler on whichever thread finished the operation, even when a GUI thread set it, so hand the result to the thread that uses it as for an event handler. If the operation had already finished, the handler is called at once, on the thread that sets the property.
You must also be careful about not creating a reference cycle to the operation, otherwise it will cause a memory leak. This can happen if the callback is a closure and references an object that references the operation itself.
Progress¶
An operation that reports progress has a progress property. Setting it gives
the operation its progress handler, a delegate that WinRT calls
with the operation and the progress value each time there is progress to
report. An operation has one progress handler, so setting the property again
replaces it. Set the handler before awaiting the operation, so that no report
is missed.
WinRT calls the handler on a thread of its own, as it does an
event handler, so with asyncio hand the
value to the event loop with loop.call_soon_threadsafe():
async def download(operation) -> None:
loop = asyncio.get_running_loop()
def on_progress(value) -> None:
print("progress:", value)
operation.progress = lambda op, value: loop.call_soon_threadsafe(
on_progress, value
)
await operation
Buffers¶
A method parameter of type Windows.Storage.Streams.IBuffer takes
any Python object that implements the buffer protocol, for example, a
bytearray,
bytes, or a memoryview, and WinRT reads or writes
its memory directly, without a copy. The type hints name such a parameter
winrt.system.ReadableBuffer, or
winrt.system.WriteableBuffer where WinRT
writes into it, both aliases for collections.abc.Buffer.
An IBuffer that WinRT hands back is typed as IBuffer, which supports the
buffer protocol and len() too and has length and capacity.
Where WinRT writes into the buffer, as read_async() of a stream does, the
object has to be writable, so a bytearray rather than bytes, and a read-only
one raises TypeError. Use the buffer the method hands back: its
len() is how many bytes WinRT wrote, which can be fewer than the object holds.
data = bytearray(1024)
read = await stream.read_async(data, len(data), InputStreamOptions.NONE)
received = bytes(read) # the len(read) bytes the read filled
For a small read, copying the bytes out with bytes() is quickest. For a large
one, from tens of kilobytes, a memoryview of read is quicker:
it reaches the same bytes without copying them, for as long as the view is open.
WinRT's metadata does not say which methods write into a buffer, so PyWinRT
knows those of the Windows SDK by name and takes any other method to only read
its buffer. If such a method sets the length of a read-only one after all, it
fails with PermissionError rather than passing silently.
For the pixels of a bitmap, SoftwareBitmap.lock_buffer(), below, reaches the
bitmap's own memory, where copy_to_buffer() copies all of it.
Buffers received as a return value can likewise be used with anything that
supports the buffer protocol. For example, memoryview can be used to
access the buffer memory directly. Or, the struct module can be used to unpack
formatted binary data.
Using native Python classes to access the memory is significantly more efficient than using the WinRT Windows.Storage.Streams.IDataReader or Windows.Storage.Streams.IDataWriter classes.
A Windows.Foundation.IMemoryBuffer - a MemoryBuffer, the
BitmapBuffer of SoftwareBitmap.lock_buffer(), the AudioBuffer of
AudioFrame.lock_buffer(), a tensor - supports the buffer protocol too, so
memoryview or numpy.frombuffer() reads and writes its memory
directly. A view keeps the memory for as long as it exists, whatever is done
with the buffer meanwhile, so it may outlive the with block that closes the
buffer. A SoftwareBitmap stays locked while a view of its BitmapBuffer
exists. A view needs no reference, so the type hints mark create_reference()
deprecated and point at the buffer protocol instead. A reference from it holds
the buffer until it is closed, and closing it while a view of it exists raises
BufferError.
Changed in version 4.0
An IMemoryBuffer supports the buffer protocol itself, and the type hints
mark create_reference() deprecated in its favor.
A Windows.Foundation.IMemoryBufferReference supports the buffer protocol as
well, and is what WebView2's CoreWebView2SharedBuffer.buffer returns. Closing
the reference frees the memory once the memory buffer is closed or released
too, so while a memoryview or a NumPy array of the reference
exists, closing it raises BufferError, as closing an
mmap does, and the reference stays open. Release the view, or copy
what it holds, before the end of a with block that closes the reference. A
close made by WinRT itself cannot be refused.
len() of a buffer is its length in bytes, and of a memory buffer reference its
capacity, which is the length of a memoryview of either. So
an empty one is false, as an empty bytes is: a new Buffer(16) is false
until its length is set, and so is a closed memory buffer reference. Compare
with None to ask whether there is a buffer at all.
Changed in version 4.0
A buffer and a memory buffer reference have a len(), and are false when
they are empty. They had no len() and were always true.
Collections¶
Most of the generic types in Windows.Foundation.Collections are projected as collections.abc types. The WinRT runtime types are still present behind the scenes, but the type hints use only the Python standard library types to encourage users to use the Pythonic APIs. Furthermore, types that inherit from these interfaces are also extended to support the Pythonic APIs.
Where WinRT takes a collection of strings, such as an IIterable<String> or
an IVector<String>, a str raises TypeError, although it is
an iterable of strings: it would be read as one string for each of its
characters. Pass a list of one string instead, such as
DeviceInformation.create_watcher(aqs, ["System.ItemNameDisplay"]).
Changed in version 4.0
A str given for a collection of strings raises TypeError. Before, it was
read as one string for each of its characters.
Sequences¶
IVector<T> is projected as MutableSequence[T] and
IVectorView<T> is projected as Sequence[T]. These types
behave very much like Python lists, so you can iterate them with a for loop, index
them seq[0], slice them seq[:3] use them with len(seq) and search with
value in seq.
A slice is a winrt.system.Array, which is a Sequence as well, so
it can be indexed, sliced and searched the same way.
For mutable sequences, items can be modified with seq[0] = value, deleted
with del seq[0], appended with seq.append(value) or seq.extend(seq2),
inserted with seq.insert(0, value), removed with seq.remove(value), and
cleared with seq.clear(). You can also reverse the sequence in place with
seq.reverse(), pop an item at a specific index using seq.pop(index)
(or the last item with seq.pop()), or add items using the += operator
like seq += [value1, value2].
A slice of a mutable sequence can be replaced with seq[1:3] = [value], by
as many items as it holds or by more or fewer, and deleted with
del seq[1:3]. A slice with a step is not supported, here as when reading
one, and raises NotImplementedError. The sequence is
changed one item at a time, so an item that cannot be converted leaves the
items before it written.
Added in version 4.0
Assigning and deleting a slice.
Mappings¶
IMap<K, V> is projected as MutableMapping[K, V] and
IMapView<K, V> is projected as Mapping[K, V]. These types
behave like Python dictionaries, so you can access values with map[key] or
map.get(key), check for keys with key in map, and iterate over keys with a
for loop. You can also retrieve all keys, values, or key/value pairs using
map.keys(), map.values(), and map.items() respectively. Equality operators
(== and !=) can be used to compare mappings for equality based on their
key-value pairs. You can also use len(map) to get the number of keys in the mapping.
Like a dict, a mapping has no hash, so it cannot be a key of a dict or be
put in a set.
Changed in version 4.0
A mapping has no hash. Before, it hashed by identity, so two equal mappings had different hashes.
Note
Python iterates over the keys only which might not be what you expect if you are used to using the same types in .NET. In Python, you will write:
To get both the keys and values, use the items() method:
For mutable mappings, you can add or update items with map[key] = value,
use value = map.setdefault(key, default) to insert a key with a default
value if it doesn't exist, delete items with del map[key], and clear all
items with map.clear(). You can also remove and return a value while
removing the key using map.pop(key) or map.popitem(). Multiple values
can be set at the same time using map.update(other).
A key missing from a mapping raises KeyError, as a dictionary
does, but a key that is not of the mapping's key type raises
TypeError, even in key in map and map.get(key), as it does
for os.environ: 5 in map for a map with str keys is a
mistake to report, not a key to look for.
There is also a special handling for IIterable<IKeyValuePair<K, V>> when
used as an argument to methods where a Python mapping is allowed. This means you
can pass a Python dictionary as the argument.
Example:
Iterators¶
IIterable<T> becomes Iterable[T]. Any iterable Python
type but a str for an IIterable<String>, as above, can be
used as an argument and return values can be used as any other iterable
in Python. Don't use the WinRT first() method to get an iterator, instead use the
builtin iter() function
or other Python features like for loops.
IIterator<T> is projected as Iterator[T] however these
objects are rarely used directly in Python. Instead, use for loops or generator
expressions to do the iterating for you. In rare cases, iterators might be used with
next().
The WinRT methods on this object should be avoided.
Context managers¶
Any type that implements IClosable can (and probably should) be used as a context manager in Python:
Generally, when an object it closable, it means that there are unmanaged resources that may need to be released in a deterministic manner as opposed waiting for the garbage collector to run to clean them up.
Note
.NET programmers may recognize this as similar to using statements
in C# with IDisposable types.
Date and time¶
There are a few foundational time-related types in WinRT that are projected as the analogous Python types.
Windows.Foundation.DateTime¶
This type is converted to a datetime.datetime
object from the standard Python library.
WinRT has a resolution of 100 nanoseconds while the Python type has a resolution
of 1 microsecond, so there is a small loss of precision in the conversion. Python
also has a much smaller allowed range of dates (years from 1 to 9999), so a
value from WinRT outside those years raises OverflowError.
WinRT serializes values of this type as 100s of nanoseconds since January 1, 1601 (UTC).
It uses a signed 64-bit integer for this, so the "q" format string is used in Python.
WinRT uses UTC for all values, so any datetime object returned from a Windows
API will use that timezone. It is recommended to use the UTC timezone when
creating datetime objects to pass to Windows APIs as well. "Naive" datetime
objects (without a timezone) are assumed to use the local timezone and will
be converted to UTC. On Windows, Python can find the local time only for a
limited range of years, so a naive datetime far in the past or future raises
an error from Python itself. An aware datetime converts for every year from 1
to 9999.
Example:
# set notification to expire 10 seconds from now
toaster.expiration_time = datetime.now(timezone.utc) + timedelta(seconds=10)
Windows.Foundation.TimeSpan¶
This type is converted to a datetime.timedelta
object from the standard Python library.
WinRT has a resolution of 100 nanoseconds while the Python type has a resolution
of 1 microsecond, so there is a small loss of precision in the conversion. WinRT
also has a much smaller range, about 29,000 years either way (10,675,199 days),
so a longer timedelta, such as timedelta.max, raises
OverflowError.
WinRT serializes values of this type as 100s of nanoseconds.
It uses a signed 64-bit integer for this, so the "q" format string is used in Python.
Error codes and event tokens¶
Two structs in WinRT hold a single integer, and are projected as subclasses of
int, so they compare, hash and format as the integer they hold.
Windows.Foundation.HResult¶
An error code that WinRT hands back as a value rather than raising, such as an
async operation's error_code. It compares with
OSError.winerror and with the codes in
winrt.system.hresult directly.
A code is a signed 32-bit integer, which is how WinRT hands one back, so
E_FAIL is -2147467259. HResult() takes one integer, in that form or in
the unsigned form a code is usually written in, so
HResult(0x80004005) == E_FAIL. Anything that is not a 32-bit code in either
form raises OverflowError.
Windows.Foundation.EventRegistrationToken¶
What add_<event>() returns and remove_<event>() takes. See
Events. Only WinRT issues one, so creating one from Python raises
TypeError.
Changed in version 4.0
HResult and EventRegistrationToken are subclasses of int. Previously
each was a struct with a single value field, which is deprecated and
raises a DeprecationWarning.
Numerics¶
The structs of Windows.Foundation.Numerics - Vector2, Vector3,
Vector4, Quaternion, Plane, Matrix3x2 and Matrix4x4 - have the
arithmetic, the methods and the constants that
System.Numerics gives them in .NET, such as
v.length(), v.dot(w), Vector3.unit_x and Matrix4x4.make_rotation_z().
Their fields are 32-bit floats, so a value read back from one is rounded to
float32 precision.
The operators:
| Expression | Meaning |
|---|---|
v + w, v - w, -v |
Elementwise, for the vectors, the quaternion and the matrices. |
v * w, v / w |
Elementwise, for two vectors of the same type. |
v * 2, 2 * v, v / 2 |
Scaling by a number, on either side; a matrix or a quaternion is multiplied by a number but not divided. |
a @ b |
The matrix product of two matrices of the same type. |
q * r, q @ r |
The Hamilton product of two quaternions, which composes their rotations; q.concatenate(r) is r * q. |
v @ m |
v.transform(m): a vector by a Matrix4x4 or a Quaternion, and a Vector2 by a Matrix3x2 too. |
abs(v) |
v.length(), for the vectors and the quaternion. |
* between two matrices raises TypeError, so that it is never
taken for either the matrix product or an elementwise product. Otherwise an
operator takes a value of the struct, or a number where it scales, and with
anything else it leaves the answer to the other operand. So a struct and a
NumPy array give NumPy's answer either way round, and a tuple raises
TypeError, although a method takes a tuple of a struct's fields in place of
the struct.
The library uses row vectors: a vector is transformed by putting it on the
left of a matrix, and a translation is in the last row of the matrix, in
m41, m42 and m43. A matrix on the left of a vector is not supported.
Since transforms compose from left to right, v @ (a @ b) applies a and
then b.
from math import pi
from winrt.windows.foundation.numerics import Matrix4x4, Vector3
move = Matrix4x4.make_translation(10, 0, 0)
turn = Matrix4x4.make_rotation_z(pi / 2)
point = Vector3(1, 0, 0) @ (turn @ move) # turned, then moved: (10, 1, 0)
A vector or a quaternion is a sequence of its components, so it unpacks and
works with len(), sum(), max() and the like. A
matrix is indexed by a row and a column, counted from 0: m[3, 0] is m.m41.
unpack() gives the fields of any of them as a tuple.
NumPy¶
Each of these structs supports the buffer protocol: a read-only buffer of its
floats, in the shape (2,), (3,) or (4,) for the vectors and the
quaternion, (4,) for a Plane (the normal and then the distance), and
(3, 2) or (4, 4) for the matrices, a row after another. An
Array of them is a writable block of floats with one more
dimension, (n, 3) for an array of Vector3 and (n, 4, 4) for an array of
Matrix4x4. NumPy and anything else that reads buffers sees them
without a copy:
import numpy as np
from winrt.system import Array
from winrt.windows.foundation.numerics import Matrix4x4, Vector3
np.asarray(Matrix4x4.identity) # a (4, 4) float32 view
points = Array(Vector3, 1000)
np.asarray(points)[:, 2] = 1 # writes z of every point
The other way around, a buffer of floats or doubles in exactly the shape of a
struct is taken wherever a method takes the struct, as a tuple of its fields
is: a NumPy array of shape (3,) stands for a Vector3 and one of shape
(4, 4) for a Matrix4x4. The exception is a method whose overloads take
different structs, such as transform(), which picks what to do by the type of
the struct it is given, so it takes the struct itself and not a tuple or a
buffer. An operator takes neither, as above. An Array of one of the structs is made from, and an array
parameter of one takes, a block of float32 values of shape (n, 3),
(n, 4, 4) and so on:
Flat floats are not taken for a shape, as NumPy does not take them for one
either: sixteen floats in a row could be the rows of a matrix or its columns.
Give them their shape with reshape(), or with
memoryview(data).cast("B").cast("f", (4, 4)) for an
array.array. A tuple is
another thing: it is the fields of the struct, in the order its constructor
takes them, so a Matrix4x4 is sixteen numbers in a tuple as it is in
Matrix4x4(...).
A value of one of the structs, or an Array of them, is not taken as another
of the structs, although its floats would fit, because a Quaternion is not
a Vector4 and three Vector2 are not a Matrix3x2. To read its floats as
another one on purpose, pass np.asarray(value) or memoryview(value), which
are plain floats:
from winrt.windows.foundation.numerics import Quaternion, Vector4
rotations = Array(Quaternion, 2)
Array(Vector4, rotations) # TypeError
Array(Vector4, memoryview(rotations)) # the same floats, as two Vector4
Changed in version 4.0
* between two matrices was the matrix product, and is a TypeError;
the product is @. The structs export a buffer, the vectors and the
quaternion are sequences, a matrix takes m[row, column], a number
multiplies a matrix or a quaternion from either side, and a buffer is
taken in place of a struct where a method takes one. An operator took a
tuple of a struct's fields in place of the struct, and takes only the
struct. An Array of these structs exported a buffer of one item per
element with a named field per float, and exports a block of floats.
-
Strings that are converted to
Char16can only contain one character, similar to howord()works. ↩ -
"u"is deprecated in thearraymodule and is not compatible with thestructmodule. Use"H"instead if needed, which is whatwinrt.system.StructFormatcarries forChar16. ↩ -
Use
"I2H8B"with thestructmodule since it does not support the PEP 3118T{}syntax. ↩