Skip to main content
Pre-release

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

dynwinrt-codegen

Generate typed Python bindings for Windows Runtime (WinRT) APIs from .winmd metadata.

dynwinrt-codegen reads the metadata shipped by the Windows SDK, WinAppSDK, and other Windows components. It emits Python modules that use dynwinrt to invoke those APIs at runtime. The generated API uses Python naming, values, collections, type annotations, and asyncio-compatible operations.

Why use this?

Calling a WinRT API without an existing Python projection normally requires a native extension or handwritten metadata, COM ABI, and marshaling code. dynwinrt-codegen derives that information from .winmd files and generates:

  • Python classes with snake_case properties and methods
  • type-checked overloads and .pyi type stubs
  • asyncio-compatible WinRT operations
  • Python-native collections, GUIDs, dates, times, and byte arrays
  • enums, structs, delegates, and event helpers
  • a package manifest pinned to the matching dynwinrt runtime version

The generator is a standalone Windows executable. Installing or running it does not require Cargo or Rust.

Install and generate

python -m pip install --pre dynwinrt-codegen

# Generate one Windows SDK class.
dynwinrt-codegen generate `
  --namespace Windows.Foundation `
  --class-name Uri `
  --lang py `
  --output .\generated_uri

# Install the generated package and its exact dynwinrt runtime dependency.
python -m pip install .\generated_uri

The generated package can then be imported normally:

from dynwinrt import RoApartment, projected_lifetime_scope
from generated_uri.windows.foundation import Uri

with RoApartment(), projected_lifetime_scope():
    uri = Uri("https://example.com/path")
    print(uri.host)

The matching Python binding also tracks raw native DynWinRTValue results from generated factories and methods in an active lifetime scope. For example, PropertyValue.create_uint32(8080) returns a raw value; if it escapes the scope, its owned COM reference has already been released before the apartment exits. A raw result retained without a scope must instead be released explicitly inside its apartment.

CLI options

Option Description
--winmd PATH[;PATH...] Metadata file paths. Sibling .winmd files are discovered automatically. The Windows SDK is auto-detected when no input supplies Windows.* metadata.
--winmd-list FILE Newline-separated metadata paths to emit; blank lines and # comments are ignored.
--folder DIR Load every .winmd file directly inside a directory.
--namespace NS Generate one namespace. Without it, generate all non-Windows.* namespaces in the input.
--class-name NAME[,NAME...] Generate specific classes or public interfaces. Use fully qualified names, or unqualified names together with --namespace.
--ref PATH[;PATH...] Metadata used only for type resolution. Sibling discovery is disabled for references.
--ref-list FILE Newline-separated reference metadata paths; blank lines and # comments are ignored.
--output DIR Dedicated codegen-owned output directory (default ./generated). Existing contents may be replaced or removed.
--dry-run Validate metadata and dependencies without writing files.
--pyi Explicitly request the default Python type stubs; retained for compatibility.
--no-pyi Omit .pyi files and the py.typed marker.
--winappsdk-version MAJOR.MINOR Stable SDK product version, such as 2.5, not a NuGet servicing version. Requires --winappsdk-bootstrap-path.
--winappsdk-bootstrap-path PATH Bootstrap DLL file or recursively searched directory; repeat for multiple inputs. Requires --winappsdk-version.

Use --lang py for every Python generation command. Run dynwinrt-codegen generate --help for the complete command reference.

Windows App SDK configuration

Pass the paired options when generating bindings for an application with restored Windows App SDK inputs:

dynwinrt-codegen generate `
  --winmd-list .\winmd-inputs.txt --ref-list .\winmd-references.txt `
  --lang py --output .\app_bindings `
  --winappsdk-version 2.5 `
  --winappsdk-bootstrap-path '.winapp\bin'
python -m pip install .\app_bindings

Supply a directory to search for Microsoft.WindowsAppRuntime.Bootstrap.dll within it and its descendants, or concrete DLL files; repeat the option to mix inputs. Discovery does not search outside the supplied directories or follow directory links/reparse points. Architecture is read from each DLL, not inferred from its name or directory. Unsupported directory matches such as x86 are reported and ignored; explicit unsupported files still fail. A directory with no supported DLL, an unreadable directory, or a corrupt/unreadable matching file fails rather than producing a partial bundle. Byte-identical duplicates are deduplicated without losing their source licenses; different DLL bytes for the same architecture are an error. Reordering paths or combining overlapping files/directories does not change the generated configuration or build-cache digest.

The generated package contains _winappsdk.py, _winappsdk.json, the unchanged DLLs under _native/winappsdk/x64 and/or arm64, and redistribution notices. It declares an installed dynwinrt.winappsdk entry point. The application can call dynwinrt.init_winappsdk() without passing a project root, version, path, or environment variable, even before importing the generated projection. Initialization remains an explicit application action.

Normal wheel installs and pip install -e work independently of the runtime working directory. A generated source folder added only to sys.path, without installing its distribution metadata, is not automatically discovered; the runtime's explicit version/path API remains available. Moving an editable source tree requires reinstalling it. Zip imports and frozen-app resource extraction are not supported by this configuration provider.

The generated wheel is Windows/platform-specific, with a compressed py3-none-win_amd64.win_arm64 tag when both DLLs are present. A small build-only setuptools hook supplies the tag and is retained in sdists, not installed as runtime code. Matching dynwinrt CPython 3.11-3.14 wheels are still required.

MAJOR.MINOR identifies the stable product, not a NuGet patch version or the DLL's FileVersion. Metadata/configuration presence does not prove that an application has used an App SDK API or that Runtime deployment is ready. Repeated generation preserves an omitted configuration, including --no-pyi; changing its version or bytes requires clean, complete regeneration. See bundle validation, capabilities and licensing and the runtime initialization contract.

More examples

Generate two classes from the Windows SDK:

dynwinrt-codegen generate `
  --namespace Windows.Storage `
  --class-name StorageFile,StorageFolder `
  --lang py `
  --output .\storage_bindings

Generate all non-system namespaces from a restored metadata folder:

dynwinrt-codegen generate `
  --folder C:\path\to\metadata `
  --lang py `
  --output .\component_bindings

Use explicit reference metadata for a reproducible generation:

dynwinrt-codegen generate `
  --winmd-list .\winmd-inputs.txt `
  --ref-list .\winmd-references.txt `
  --lang py `
  --output .\component_bindings

Validate a request without changing its output directory:

dynwinrt-codegen generate `
  --folder C:\path\to\metadata `
  --lang py `
  --output .\component_bindings `
  --dry-run

Generated output

The output is an installable Python package. Its generated pyproject.toml pins dynwinrt to the generator's exact version and requires CPython 3.11–3.14. .pyi files and a py.typed marker are emitted by default.

--output is the installable project root, not the import package directory:

generated_uri/
  pyproject.toml
  setup.cfg
  .dynwinrt-generator.json
  .dynwinrt-generated-files
  .dynwinrt-generated-types
  src/
    generated_uri/
      __init__.py
      __init__.pyi
      py.typed
      _runtime.py
      _typing.pyi
      windows/
      windows__foundation__uri.py
      windows__foundation__uri.pyi

The import/distribution names still come from the normalized final output leaf (My Bindings becomes import my_bindings and distribution my-bindings). Setuptools uses the global package-dir = { "" = "src" } mapping. A default editable install adds the source directory through an ordinary path .pth; no dynamic editable finder, custom import hook, or strict/compat mode is needed. Use python -m pip install -e .\generated_uri for development. uv supports uv pip install -e .\generated_uri or a local editable dependency with uv sync; Poetry supports a local path dependency with develop = true. Select that environment in the editor and type checker.

Bootstrap projects additionally keep MANIFEST.in and .dynwinrt-winappsdk.json at the project root, and the build-only hook at src/_dynwinrt_build/wheel.py. The provider, JSON configuration, DLLs and notices are inside src/<import_name>. The hook is included in sdists but excluded from runtime wheels; neither src nor the hook becomes an installed runtime package.

Transitive metadata dependencies are resolved automatically. Namespace packages and imports mirror the metadata hierarchy, while public members use Python snake_case naming. XML documentation found beside the input metadata is included when available.

Generated async methods return typed awaitable objects. WinRT collections implement standard collections.abc protocols, flags use enum.IntFlag, and compatible method inputs accept native Python sequences, mappings, bytes, bytearray, uuid.UUID, datetime.datetime, and datetime.timedelta.

Interface protocols describe native instances, including their canonical identity and methods, not wrapper factories. For example, StorageFile can be passed to an IStorageFile parameter without defining from_value. IStorageFile.from_value(raw) and value.as_interface(IStorageFile) remain available for explicit, IID-checked projection. Inherited from_value factories retain the receiving interface subclass: TaggedBuffer.from_value(raw) and value.as_interface(TaggedBuffer) return TaggedBuffer, not IBuffer. Independent static factories such as IBuffer.from_bytes keep their declared base-interface result. Direct runtime construction such as IBuffer(raw) also QueryInterface-checks the IID before retaining or caching a native pointer. It raises E_NOINTERFACE for a mismatched object and does not release the caller's raw value on a cache hit; the resulting view owns a separate reference. A DynWinRTValue annotation alone cannot establish the runtime IID, so prefer from_value() or as_interface() for explicit intent.

WinRT Object inputs accept a DynWinRTValue or a projected native wrapper whose _obj is a DynWinRTValue, including interface views and runtime-class Like views. They do not implicitly box arbitrary Python objects, strings, or None; use an explicitly boxed or null DynWinRTValue instead. Object outputs remain DynWinRTValue | None. Context managers retain the entered instance's type (Self), including derived wrappers passed through a base Like protocol, and still close the object without suppressing exceptions.

Collection subscripts use the input contract for keys and values: for example, properties["uri"] = uri accepts a generated Uri, while reading the item still returns DynWinRTValue | None. Sequence item assignment, slice assignment, and insert likewise accept projected inputs without changing their read types; integer indices take one item and slices take an iterable of items. Generic nullable collections.abc contracts remain unchanged. The stock JsonArray and JsonObject classes instead have non-null IJsonValue element/value contracts in both stubs and runtime method input annotations (including --no-pyi output). Their native implementations reject None (including through generic interface views) before mutation; use JsonValue.create_null_value() for JSON semantic null. A custom IVector<IJsonValue> or IMap<String, IJsonValue> may still store a native null. For other nullable WinRT positions, pass DynWinRTValue.null_value() rather than implicit None boxing.

The output directory belongs to codegen; do not store handwritten files in it. After changing metadata files, SDK versions, or reference inputs, regenerate the complete output. Regenerate with the updated generator to pick up these consumer typing contracts, and use the matching runtime package and its stubs. No runtime API change or consumer cast is required.

Producer version and regeneration

.dynwinrt-generator.json records the package layout/import identity and generator_version, the complete generator package version, including any prerelease/preview suffix. This is a producer stamp, not the generated distribution version, runtime dependency version, or the type inventory's schema_version.

New/empty output and non-generated user content need no stamp. Existing generated output must have a valid stamp matching the running generator exactly. Same-version generation retains the existing incremental behavior. A different version (including a downgrade), missing stamp on generated artifacts, or corrupt stamp causes an unsuccessful exit before existing output is changed, also in --dry-run. Manually clean the dedicated output directory and regenerate the entire bindings selection, or use a fresh output directory; do not remove or edit only the stamp. The generator never automatically deletes or migrates incompatible output.

Every generator upgrade therefore requires full regeneration, followed by rebuilding/reinstalling the affected packages with the matching runtime. Unversioned development changes other than recorded layout changes are not detected by this check; use fresh output for those changes too.

Breaking layout upgrade from preview.23 and earlier

Older releases placed Python modules directly in --output and mapped a named package to ".". Clean that dedicated output and regenerate the whole selection with the new generator, then reinstall it with the matching runtime. Old-layout output is rejected even when its producer version string matches; do not move only some files or edit/remove the producer stamp to bypass the guard. Installed namespace, flat-module and root imports, the installation command, runtime version pin and native ABI are unchanged.

Replace old examples that add output.parent to sys.path/PYTHONPATH with a normal or editable installation. For deliberate uninstalled source use, the search directory is now <output>/src, not <output> or its parent. Source-only imports still do not provide distribution metadata for Bootstrap discovery.

Regeneration at the same project path is atomic. An existing editable install sees added types in a new Python process and in a new checker run without reinstallation; already imported modules are not hot-reloaded. Reinstall after moving/renaming the project, changing package/dependency versions, or adding or changing entry points. If the output leaf is renamed, the next generation relocates only recorded package files inside the transaction, rejects destination conflicts, updates manifests and retains the semantic type inventory. It does not leave a runtime alias under the previous import name.

Closed-generic stub migration

Python stubs now identify closed generic interfaces by their complete semantic identity instead of a local projection name. This prevents different type arguments from becoming interchangeable when their short names collide.

Stubs generated before this fix and regenerated stubs are not type-compatible for the same closed interface in either direction. This is a typing migration: the generated .py runtime code and native interface IIDs are unchanged.

Regenerate the complete output of every generated Python package exchanging these interfaces with the fixed generator, including runtime classes that implement or require them, then rebuild/reinstall the affected packages. Use a fresh codegen-owned output directory with all original type selections and metadata/reference inputs. An incremental append that retains old .pyi declarations is not sufficient. Do not copy individual marker declarations or add legacy fallbacks: they can restore the incorrect cross-interface acceptance. The new markers agree across independently regenerated packages for the same closed identity, even when their local projection names differ.

Platform and limitations

  • The standalone generator has py3-none-win_amd64 and py3-none-win_arm64 wheels for Python 3.8–3.14.
  • Generated bindings and the dynwinrt runtime require CPython 3.11–3.14 on Windows x64 or ARM64.
  • Python generation currently supports WinRT metadata. Classic COM and flat Win32 DLL-export generation from Windows.Win32.winmd are available only for JavaScript and TypeScript.
  • Some APIs require their Windows component, package identity, or framework bootstrap to be present at runtime.

Python implementation module stems use a 56 UTF-16-code-unit budget. Public per-type module stems use the remaining budget after their namespace directories. Only over-budget names are shortened with a readable prefix and stable hash of the complete type identity. Public type names, namespace exports, and ordinary short per-type and flat-module imports remain unchanged. Direct imports of shortened long modules must migrate; no unsafe long-filename shims are emitted.

Windows path length

The relative stem budget allows up to 60 UTF-16 code units for .pyi source paths and reserves space for optimized CPython 3.11–3.14 __pycache__ filenames and their atomic-write temporary suffixes. For example, it fits the legacy 259-unit limit with a 130-unit site-packages prefix and a 16-unit package name, including a maximum-width 64-bit temporary identifier. Deep metadata namespaces may exceed this budget even with a minimal basename; their namespace hierarchy is not flattened. Generated setup.cfg also shortens the isolated setuptools build cache to .b/<layout-and-package-hash> and wheel staging to .b/<same-hash>/w, avoiding the extra default bdist.<platform>/wheel nesting. Cache isolation still tracks the source layout and import package name.

A short module name does not guarantee a short absolute path. In deep checkouts, codegen can successfully write files that ordinary Python imports, mypy, or pip cannot open. CPython supports long paths on appropriately configured Windows; see the Python Windows guide and Windows long-path requirements. Extended \\?\ paths used by the generator do not automatically make downstream tools long-path compatible; the Windows LongPathsEnabled setting alone does not prove every consumer supports them.

On Windows, the generator emits one stderr warning before publishing output when its longest final .py/.pyi path reaches 260 UTF-16 code units (the legacy limit allows 259, excluding the terminating NUL). The warning names that file, measures its path without the extended prefix, and states how much shorter the output root needs to be. Relative --output paths are resolved from the current directory; temporary transactional staging paths are not counted. --dry-run checks the requested projection's planned source paths, aggregating all selected namespaces into one warning; --no-pyi excludes stubs. This remains a compatibility risk diagnostic: generation still succeeds without a further root-dependent naming change. The deterministic module budget and compact staging do not guarantee arbitrary checkout, package-name, build-temporary-directory, or venv depths.

Prefer a shorter checkout such as C:\src\dynwinrt, or generate directly into a short root and run consumers there:

dynwinrt-codegen generate --namespace Windows.Foundation --class-name Uri `
  --lang py --output C:\g\generated_uri
Set-Location C:\g
python -c "from generated_uri.windows.foundation import Uri; print(Uri)"
python -m mypy --strict .\consumer.py
python -m pip install .\generated_uri

Here consumer.py is your application or typing fixture beside the generated package. Leave additional headroom for pip's build staging and the destination venv/site-packages path. Those paths cannot all be predicted from --output; no warning is not a guarantee that a later build or deep-venv install will work. Keep the venv short as well, for example C:\g\venv.

For an existing deep source tree, an optional, user-owned short junction can expose the same generated bytes without renaming modules or copying code:

New-Item -ItemType Directory -Path C:\g -Force | Out-Null
New-Item -ItemType Junction -Path C:\g\project -Target C:\src\deep\project
Set-Location C:\g\project
dynwinrt-codegen generate --namespace Windows.Foundation --class-name Uri `
  --lang py --output .\generated
python -c "from generated.windows.foundation import Uri; print(Uri)"
python -m mypy --strict .\consumer.py
python -m pip install .\generated
Set-Location C:\g
Remove-Item -LiteralPath C:\g\project

Link the parent checkout, not the generated directory itself: transactional generation rejects a linked output directory. Use the same shortened path for generation, imports, mypy, and package builds; returning to the deep path reintroduces its limits. Remove only the junction you created, without recursively deleting its target.

Alternatively, map an unused drive for the session with subst.exe R: C:\src\deep\project, use R:\ consistently for these commands, and remove only that mapping with subst.exe R: /D after leaving the drive. These short-path remedies have been validated with Python imports, strict mypy, and generated-package installation on Windows with long paths disabled; they do not automatically fix arbitrary deep build or install destinations.

License

MIT

Metadata

Release files for dynwinrt-codegen 0.1.0rc24

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

Built distributions (wheels)

Table of built distributions (wheels) for dynwinrt-codegen 0.1.0rc24
File Interpreter ABI Platform Actions
dynwinrt_codegen-0.1.0rc24-py3-none-win_arm64.whl Python 3 none Windows ARM64 Details for dynwinrt_codegen-0.1.0rc24-py3-none-win_arm64.whl
dynwinrt_codegen-0.1.0rc24-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details for dynwinrt_codegen-0.1.0rc24-py3-none-win_amd64.whl

Total release size: 8.3 MB

Release files / dynwinrt_codegen-0.1.0rc24-py3-none-win_arm64.whl

Download URL dynwinrt_codegen-0.1.0rc24-py3-none-win_arm64.whl
Size 3.9 MB
Tags Python 3 Windows ARM64
SHA-256 checksum
How to use checksums
a1631d8f3d7ce40833e9bd3f73fe53f3519dc1de7963725d984949aa254b6583
BLAKE2b-256 checksum
How to use checksums
4f9a727d5be31e365aaeba5b2f12c51f939d8fec5f8c61843533ba45bbfb30df
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via RestSharp/106.13.0.0

Release files / dynwinrt_codegen-0.1.0rc24-py3-none-win_amd64.whl

Download URL dynwinrt_codegen-0.1.0rc24-py3-none-win_amd64.whl
Size 4.4 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
6d7562929ae20c0e3363b82c75d92c3743bce1ea04aa89f0af0d70e8836662fc
BLAKE2b-256 checksum
How to use checksums
3717a12fcdd3f08cc5b5b0938c9b7f726c145a64f4d42ed735f033c4db0e1ddd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via RestSharp/106.13.0.0

Security for dynwinrt-codegen 0.1.0rc24

Provenance

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

No provenance found

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

A note on package security

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

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

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