<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Zoltan Csizmadia</title>
    <description>The latest articles on DEV Community by Zoltan Csizmadia (@zcsizmadia).</description>
    <link>https://dev.to/zcsizmadia</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3952814%2F93486256-a133-460a-807d-ab4168717373.png</url>
      <title>DEV Community: Zoltan Csizmadia</title>
      <link>https://dev.to/zcsizmadia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zcsizmadia"/>
    <language>en</language>
    <item>
      <title>Docker Desktop costs money now. Your Windows box already has the engine.</title>
      <dc:creator>Zoltan Csizmadia</dc:creator>
      <pubDate>Thu, 17 Sep 2026 19:44:42 +0000</pubDate>
      <link>https://dev.to/zcsizmadia/docker-desktop-costs-money-now-your-windows-box-already-has-the-engine-5c2e</link>
      <guid>https://dev.to/zcsizmadia/docker-desktop-costs-money-now-your-windows-box-already-has-the-engine-5c2e</guid>
      <description>&lt;p&gt;Since 2021, Docker Desktop has needed a paid subscription at companies past a certain size. A lot of developers have been told to stop using it, and the honest replacement list is shorter than it looks.&lt;/p&gt;

&lt;p&gt;Here is the thing that took me embarrassingly long to internalize: &lt;strong&gt;the engine was never the licensed part.&lt;/strong&gt; &lt;code&gt;dockerd&lt;/code&gt; is Apache-2.0. It runs perfectly well in WSL2. What Docker Desktop sells you is the desktop application around it — the GUI, the VM management, the update channel, the support.&lt;/p&gt;

&lt;p&gt;If you do not want any of that, the remaining work is smaller than it sounds. You need the engine in a distro, and you need to serve its API on &lt;code&gt;\\.\pipe\docker_engine&lt;/code&gt; — the pipe &lt;code&gt;docker.exe&lt;/code&gt; already talks to. Get that right and Compose, buildx, Testcontainers, Dev Containers and everything else keep working without knowing anything changed.&lt;/p&gt;

&lt;p&gt;That is what &lt;a href="https://github.com/wslkit/skrog" rel="noopener noreferrer"&gt;Skrog&lt;/a&gt; is. No Electron, no Kubernetes, no tray icon you have to keep running. Install it once and &lt;code&gt;docker ps&lt;/code&gt; works forever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;irm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://wslkit.github.io/skrog/install.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;skrog&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--rm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hello-world&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under five minutes on a clean Windows 11 box, and you never touch WSL directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lifecycle.&lt;/strong&gt; WSL2 is a VM that disappears. &lt;code&gt;wsl --shutdown&lt;/code&gt; kills it. Sleep/resume breaks it. The engine crashes. If any of those leave &lt;code&gt;docker&lt;/code&gt; broken until the user runs something by hand, you have built a demo, not a tool. Skrog runs a supervisor that starts at logon, watches the engine, and is itself watched — if the supervisor dies, a small launcher restarts it in about a second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idle RAM.&lt;/strong&gt; The most common complaint about every WSL2-based engine is that &lt;code&gt;vmmem&lt;/code&gt; sits there holding a gigabyte while you are not using Docker. &lt;code&gt;skrog config set idle-timeout 30m&lt;/code&gt; stops a quiet engine and cold-starts it on your next &lt;code&gt;docker&lt;/code&gt; command.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timely bit: Microsoft now ships a container runtime, and you still can't reach it
&lt;/h2&gt;

&lt;p&gt;If you have updated WSL recently, you already have &lt;code&gt;wslc.exe&lt;/code&gt; — Microsoft's own container runtime, shipping in WSL 2.9.3+ and headed for GA. It is the strongest argument against this project existing, so it is worth being precise about.&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;wslc&lt;/code&gt; session really does run a genuine Moby engine. I went and checked: stock &lt;code&gt;dockerd&lt;/code&gt; on a unix socket inside the session VM. Real engine, not a reimplementation.&lt;/p&gt;

&lt;p&gt;But it is started with &lt;strong&gt;no &lt;code&gt;-H&lt;/code&gt; flag&lt;/strong&gt;. No named pipe, no TCP port, no inbound route from Windows. The engine is complete and unreachable.&lt;/p&gt;

&lt;p&gt;That means anything that speaks the Docker API cannot talk to it: Compose, Testcontainers, Dev Containers, buildx, &lt;code&gt;act&lt;/code&gt;, Dagger, anything that mounts &lt;code&gt;docker.sock&lt;/code&gt;. &lt;code&gt;wslc&lt;/code&gt; mimics the docker &lt;em&gt;CLI&lt;/em&gt;, which covers &lt;code&gt;build&lt;/code&gt; and &lt;code&gt;run&lt;/code&gt; and stops there.&lt;/p&gt;

&lt;p&gt;That gap — a real engine with no endpoint — is the entire reason this project has a reason to exist. Skrog serves the endpoint. It can even serve &lt;em&gt;wslc's&lt;/em&gt; engine, if you would rather Microsoft owned the runtime: &lt;code&gt;skrog install --engine wslc&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  So which one is actually faster?
&lt;/h3&gt;

&lt;p&gt;I benchmarked both on one host, because "we're fast" is not a claim, it is a vibe.&lt;/p&gt;

&lt;p&gt;Windows 10 Pro 22H2 (19045), WSL 2.9.11, 4 vCPU / 7.8 GB visible to each VM. Skrog with engine 29.8.1 on its vsock transport, &lt;code&gt;/mnt/c&lt;/code&gt; on 9p; a wslc session running the &lt;code&gt;dockerd&lt;/code&gt; Microsoft ships. Control-plane timings are wall-clock around each shipped CLI, so process spawn is included on both sides; filesystem numbers are timed &lt;em&gt;inside&lt;/em&gt; the container, so they are not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Control plane&lt;/strong&gt; — wslc wins most of it, and it should:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;wslc&lt;/th&gt;
&lt;th&gt;Skrog&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;list containers, median of 10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;53 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;74 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;list images, median of 10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;57 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;276 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;exec &amp;lt;running&amp;gt; true&lt;/code&gt;, median of 10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;109 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;155 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;run --rm alpine true&lt;/code&gt; warm, median of 5&lt;/td&gt;
&lt;td&gt;538 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;528 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docker version&lt;/code&gt; round trip&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;89 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That ordering is architecture, not effort. &lt;code&gt;wslc&lt;/code&gt; goes COM-to-VM with no bridge in the path; Skrog serves a named pipe and relays over vsock. Twenty to forty milliseconds of the gap is the bridge, and I would rather pay it than not have an endpoint. Actually running a container — the thing you do all day — is a tie.&lt;/p&gt;

&lt;p&gt;The 276 ms for &lt;code&gt;list images&lt;/code&gt; is the outlier, and I assumed it was my bug. It is worth showing how that went, because the instinct was wrong.&lt;/p&gt;

&lt;p&gt;First guess: payload size. &lt;code&gt;/images/json&lt;/code&gt; returns 7 838 bytes on this machine and &lt;code&gt;/containers/json&lt;/code&gt; returns 7 052. Same order. Not that.&lt;/p&gt;

&lt;p&gt;Second guess: something in my bridge doing per-image work — there is a reference parser in that path for admission control. So I cut the bridge out entirely and hit the engine's unix socket from a container, 20 requests each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;20x /containers/json   real  0m 0.97s     # 48 ms each
20x /images/json       real  0m 3.20s     # 160 ms each
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same 3.3× ratio, no Windows, no named pipe, no vsock, no Skrog code in the path at all. It is &lt;code&gt;dockerd&lt;/code&gt; computing image sizes, and it would cost the same in Docker Desktop. wslc is quicker here because it asks a different, cheaper question, not because its plumbing is better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows-folder bind mounts&lt;/strong&gt; — wslc wins, because it mounts them over virtiofs while a WSL2 distro defaults to 9p:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;wslc (virtiofs)&lt;/th&gt;
&lt;th&gt;Skrog (9p)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;read 256 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;760 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;192 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;write 256 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;155 MB/s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;103 MB/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;create 1000 files&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.59 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.39 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ls -l&lt;/code&gt; 1000 files&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.24 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.73 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;read 1000 files&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.37 s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.65 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It is also closeable without me writing any code: WSL 2.9 will give a &lt;em&gt;distro&lt;/em&gt; virtiofs too, behind a &lt;code&gt;.wslconfig&lt;/code&gt; key. &lt;code&gt;skrog config set wsl.virtiofs true&lt;/code&gt; and a &lt;code&gt;wsl --shutdown&lt;/code&gt; puts Skrog at 811 MB/s read and 0.22 s for &lt;code&gt;ls -l&lt;/code&gt; — level with wslc, and 4× its own 9p. (Those two figures come from a separate run documented &lt;a href="https://github.com/wslkit/skrog/blob/main/docs/vm-sizing.md" rel="noopener noreferrer"&gt;here&lt;/a&gt;; flipping the key needs a &lt;code&gt;wsl --shutdown&lt;/code&gt;, which stops every distro on the machine, so I did not do it mid-benchmark.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Named volumes&lt;/strong&gt; — where real workloads actually keep their data, Skrog wins:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;wslc&lt;/th&gt;
&lt;th&gt;Skrog&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;write 256 MB&lt;/td&gt;
&lt;td&gt;1.5 GB/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.7-2.1 GB/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;read 256 MB&lt;/td&gt;
&lt;td&gt;8.6 GB/s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.9-8.8 GB/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;create 1000 files&lt;/td&gt;
&lt;td&gt;0.07 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.04 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Published ports&lt;/strong&gt; — wslc wins: 574-647 MB/s against 380-416 MB/s on a 256 MB download, and 2.8 ms against 4.8 ms on a small GET, averaged over 20.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory&lt;/strong&gt; — the architectural one. A wslc session is &lt;strong&gt;its own VM&lt;/strong&gt;, and I measured ~750 MB of working set for it. Skrog's engine lives in the WSL2 utility VM you already have, next to your Ubuntu, so it adds an engine (451 MB used inside the guest, against the session's 230 MB) rather than a second VM. Windows-side, Skrog is 23 MB plus a 6 MB watchdog.&lt;/p&gt;

&lt;p&gt;If you already run WSL, that is the difference between one VM and two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The summary I would give a colleague:&lt;/strong&gt; wslc is quicker on control-plane calls, published ports and cold start; Skrog is quicker on named volumes and cheaper on memory; bind mounts go to wslc unless you flip one config key, and then they tie. If those were the only differences you should probably take the first-party option. They are not the only difference — the difference is whether &lt;code&gt;docker compose up&lt;/code&gt; works at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things Skrog does that Docker Desktop does not
&lt;/h2&gt;

&lt;p&gt;Dropping the desktop app is usually framed as a sacrifice. Some of it is — see the next section. But a headless engine you own outright can do things a managed desktop application will not, and these are the ones people actually end up using.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pin the engine, exactly.&lt;/strong&gt; &lt;code&gt;skrog lock&lt;/code&gt; writes the precise dockerd, containerd, runc and BuildKit commits to a file you commit. &lt;code&gt;setup-skrog&lt;/code&gt; installs that same file on your runner, and &lt;code&gt;skrog install --locked&lt;/code&gt; installs it on a laptop. "Works locally, fails in CI" caused by engine drift stops being a category of bug. Docker Desktop ships the version it ships and updates on its own schedule — which is the same complaint from the other direction: &lt;strong&gt;nothing here auto-updates, ever, and there is no telemetry.&lt;/strong&gt; Your engine changes when you change it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;See what actually talked to your engine.&lt;/strong&gt; &lt;code&gt;skrog audit tail&lt;/code&gt; logs container-affecting API calls, because Skrog is already in the request path. &lt;code&gt;skrog audit trace -- &amp;lt;command&amp;gt;&lt;/code&gt; goes further and shows you what a given pipeline did — useful the first time a build tool creates a container you did not expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refuse things before the engine sees them.&lt;/strong&gt; A small, fixed rule vocabulary in &lt;code&gt;policy.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deny-privileged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;deny-host-namespaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;allow-registries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;registry.example.com&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.internal"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;require-digest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;allow-bind-sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;C&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;\work&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same position in the request path, so the rules apply to Compose, Testcontainers and your IDE without any of them knowing. It is &lt;strong&gt;not&lt;/strong&gt; a security boundary against someone who owns the machine — they can edit the file or point &lt;code&gt;DOCKER_HOST&lt;/code&gt; elsewhere — and the docs say so plainly. It catches mistakes, which is most of the value of writing a policy down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot the whole engine, and reset to it.&lt;/strong&gt; &lt;code&gt;skrog snapshot save golden&lt;/code&gt; captures images, containers and volumes; &lt;code&gt;skrog reset --to golden&lt;/code&gt; puts them back unconditionally. On a CI runner that is a clean, pre-warmed engine per job in seconds rather than a re-pull.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat the install as configuration.&lt;/strong&gt; &lt;code&gt;skrog install --config skrog.yaml&lt;/code&gt; is idempotent and converges a machine to a described state; &lt;code&gt;skrog config export&lt;/code&gt; writes that file out of a machine you already like. Lifecycle hooks (&lt;code&gt;post-start&lt;/code&gt;, &lt;code&gt;pre-stop&lt;/code&gt;, &lt;code&gt;on-idle-stop&lt;/code&gt;, &lt;code&gt;on-wake&lt;/code&gt;) let you hang your own scripts off engine events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hand the engine to someone else.&lt;/strong&gt; &lt;code&gt;skrog serve --tcp&lt;/code&gt; exposes it over mutual TLS, reachable only by holders of a client certificate this machine's CA signed. Off by default. On the other end, &lt;code&gt;skrog remote add&lt;/code&gt; and &lt;code&gt;remote use&lt;/code&gt; make it your &lt;code&gt;docker&lt;/code&gt;'s default in one command — which is how a laptop borrows a workstation's GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch a fleet of them.&lt;/strong&gt; &lt;code&gt;skrog status --prometheus&lt;/code&gt; emits the engine, disk, VM and bridge numbers in node_exporter's textfile format. Local only — you point your own Prometheus at it, nothing phones home.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install without a network at all.&lt;/strong&gt; &lt;code&gt;skrog bundle&lt;/code&gt; packs the engine into a zip; &lt;code&gt;skrog install --offline&lt;/code&gt; consumes it on a machine that has never seen the internet.&lt;/p&gt;

&lt;p&gt;And the boring one that people notice first: the Windows-side footprint is about &lt;strong&gt;15 MB of Go binaries&lt;/strong&gt;, not a desktop application. There is no GUI to keep running, because there is no GUI.&lt;/p&gt;

&lt;p&gt;Docker Desktop is a moving target and I have not audited every release of it, so read that list as "things I built because I wanted them and could not get them" rather than a certified feature matrix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the alternatives win
&lt;/h2&gt;

&lt;p&gt;Every project like this posts a comparison table where it wins every row. Here is a more useful one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Take it instead of Skrog when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker Desktop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You want a GUI, Windows containers, bundled Kubernetes, macOS, or a vendor with a support contract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rancher Desktop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You want a cluster or a graphical UI in the box, or you need macOS/Linux too. It is free, open source, and runs a real engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Podman Desktop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rootless-by-default matters to you, and you can live with a Docker-&lt;em&gt;compatible&lt;/em&gt; API rather than Docker's&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;wslc&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You want a first-party runtime with Microsoft behind it, and you genuinely only need &lt;code&gt;build&lt;/code&gt; and &lt;code&gt;run&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Skrog is Windows-only, has no GUI, and will never have Kubernetes. Those are not gaps to fill later; they are the scope. Because it serves the real Docker API, the frontends already exist — Portainer, lazydocker and VS Code all work against it, and &lt;code&gt;kind&lt;/code&gt; and &lt;code&gt;k3d&lt;/code&gt; run on it, without this project owning any of that.&lt;/p&gt;

&lt;p&gt;Worth noting that the engine-pinning above is not just a Desktop gap — &lt;strong&gt;none&lt;/strong&gt; of them do it. If you have ever had "works locally, fails in CI" turn out to be engine drift, that is the row that should decide this for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing I only found by testing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cross-architecture builds are broken on a stock WSL2 engine&lt;/strong&gt;, mine included. It registers no qemu handlers, so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;docker buildx build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/arm64 &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="c"&gt;#5 [2/2] RUN uname -m&lt;/span&gt;
&lt;span class="c"&gt;#5 0.224 exec /bin/sh: exec format error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker Desktop installs binfmt for you, which is why you have probably never met this. It is one privileged container to fix, and the interesting part is that &lt;code&gt;binfmt_misc&lt;/code&gt; is kernel state in the WSL2 utility VM, so it evaporates on &lt;code&gt;wsl --shutdown&lt;/code&gt;. That makes it a supervisor's job, not a documentation footnote. It is &lt;a href="https://github.com/wslkit/skrog/issues/384" rel="noopener noreferrer"&gt;issue #384&lt;/a&gt; and it is next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you use this yet?
&lt;/h2&gt;

&lt;p&gt;Plainly: &lt;strong&gt;it depends on how much a warning dialog annoys you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is a daily driver. The engine starts at logon, heals itself, survives &lt;code&gt;wsl --shutdown&lt;/code&gt; and sleep, and answers &lt;code&gt;docker&lt;/code&gt; at the same speed as Desktop. Compose, Testcontainers and Dev Containers all work — Dev Containers with no shim at all, because the bind-mount translation happens in the bridge.&lt;/p&gt;

&lt;p&gt;It is also &lt;strong&gt;one maintainer, and not code-signed.&lt;/strong&gt; SmartScreen will warn on first run.&lt;/p&gt;

&lt;p&gt;What every release &lt;em&gt;does&lt;/em&gt; carry is SLSA build provenance and a cosign-signed &lt;code&gt;SHA256SUMS&lt;/code&gt;, which tie the artifact to a workflow and a commit — two things an Authenticode signature does not give you. But SmartScreen does not read either of those, and a scary dialog is a scary dialog.&lt;/p&gt;

&lt;p&gt;If an unsigned binary from a young project is a hard no at your company, that is a reasonable position and you should wait. If you are one of the people who got the "stop using Docker Desktop" email and has been running &lt;code&gt;docker&lt;/code&gt; through a homemade script ever since, this is that script, finished.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;irm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://wslkit.github.io/skrog/install.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apache-2.0, no telemetry, nothing auto-updates. &lt;a href="https://github.com/wslkit/skrog" rel="noopener noreferrer"&gt;github.com/wslkit/skrog&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things in flight, and one of them you can help with
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A winget package is in the approval queue.&lt;/strong&gt; When it lands, the install is &lt;code&gt;winget install skrog&lt;/code&gt; and the PowerShell one-liner above becomes the fallback rather than the front door. Nothing about it is blocked on signing — winget's &lt;code&gt;portable&lt;/code&gt; type unpacks per-user — so this is just queue time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code signing is blocked on money, and that is the honest version.&lt;/strong&gt; The SignPath Foundation runs a free programme for open source projects; they declined this one for now, on the reasonable grounds that it has no established user base yet, and invited a reapplication as visibility grows. The other route is simply buying a certificate, which needs nobody's approval and costs a few hundred dollars a year.&lt;/p&gt;

&lt;p&gt;That certificate is the single thing standing between a new user and a clean first run. It also unblocks an MSI, because an unsigned installer asking for elevation is worse than a zip, and it starts SmartScreen reputation accruing — which only begins once something is signed at all.&lt;/p&gt;

&lt;p&gt;So if this saves you a Docker Desktop seat, &lt;a href="https://github.com/sponsors/zcsizmadia" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub Sponsors&lt;/strong&gt;&lt;/a&gt; is where that goes. I would rather say that plainly than bury it in a FUNDING file: it is not for my time, it is for a certificate that removes a warning dialog for everyone who installs this after you.&lt;/p&gt;

&lt;p&gt;And if you would rather contribute in the other currency — &lt;strong&gt;bug reports are genuinely more useful to me than money right now.&lt;/strong&gt; &lt;code&gt;skrog doctor --report&lt;/code&gt; produces markdown you can paste straight into an issue. The corporate-VPN cases especially, GlobalProtect and Zscaler and AnyConnect, are the ones I cannot reproduce alone on one development machine.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>wsl</category>
      <category>windows</category>
      <category>devops</category>
    </item>
    <item>
      <title>Calling Python's Async World from C#</title>
      <dc:creator>Zoltan Csizmadia</dc:creator>
      <pubDate>Tue, 26 May 2026 15:01:30 +0000</pubDate>
      <link>https://dev.to/zcsizmadia/calling-pythons-async-world-from-c-442i</link>
      <guid>https://dev.to/zcsizmadia/calling-pythons-async-world-from-c-442i</guid>
      <description>&lt;h2&gt;
  
  
  Calling Python's Async World from C#: A Deep Dive into &lt;a href="https://github.com/zcsizmadia/PyDotNet" rel="noopener noreferrer"&gt;PyDotNet's&lt;/a&gt; Async Bridge
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The problem everyone runs into eventually
&lt;/h2&gt;

&lt;p&gt;At some point, nearly every serious .NET project ends up needing Python. It might be a machine learning model that only has a Python SDK, a data science library that simply doesn't exist on NuGet, or a team that's been doing data engineering in Python for years and isn't about to rewrite it all. The question then becomes: how do you make the two languages talk to each other without the whole thing becoming a maintenance nightmare?&lt;/p&gt;

&lt;p&gt;The standard answer for a long time was to spin up a subprocess. Run &lt;code&gt;python script.py&lt;/code&gt;, capture stdout, parse it. It works, and it's simple, but it has real costs: startup time measured in hundreds of milliseconds, data that has to be serialized to text and back, and no practical way to handle streaming results or async code. For anything performance-sensitive or interactive, it quickly becomes a bottleneck.&lt;/p&gt;

&lt;p&gt;The more sophisticated approach is to use a proper in-process embedding — load the Python shared library directly and call into it via its C API. This is what &lt;a href="https://github.com/pythonnet/pythonnet" rel="noopener noreferrer"&gt;Python.NET&lt;/a&gt; (pythonnet) has been doing for years, and it deserves credit for making .NET/Python interop possible at all. But pythonnet has accumulated some rough edges over time. Its type marshaling layer uses COM-style reflection internally, which adds latency — benchmarks typically show 5–20 µs per call versus the raw C API cost. More critically for modern applications, it has no built-in support for Python's &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; model. There's no way to &lt;code&gt;await&lt;/code&gt; a Python coroutine from C# without significant boilerplate, and Python async generators — the backbone of streaming APIs — are simply not addressable. Memory management is also non-deterministic: &lt;code&gt;Py_DecRef&lt;/code&gt; gets called from .NET finalizers, which means Python objects can live far longer than expected and collection pauses can surface at unpredictable times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where &lt;a href="https://github.com/zcsizmadia/PyDotNet" rel="noopener noreferrer"&gt;PyDotNet&lt;/a&gt; fits in
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/zcsizmadia/PyDotNet" rel="noopener noreferrer"&gt;PyDotNet&lt;/a&gt; takes a different approach. It embeds CPython in-process too — that part is the same — but it was designed from the start around three ideas that pythonnet compromised on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explicit, deterministic ownership.&lt;/strong&gt; Every Python object you hold from C# is a &lt;code&gt;using&lt;/code&gt; variable. When the &lt;code&gt;using&lt;/code&gt; block ends, &lt;code&gt;Py_DecRef&lt;/code&gt; is called immediately. There are no finalizer races, no surprise GC pauses, and no lingering Python objects. If you're debugging a refcount issue, the stack tells you exactly where the object was released.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero-copy memory.&lt;/strong&gt; NumPy arrays, PyTorch tensors, Python bytearrays — anything that implements Python's buffer protocol — can be accessed from C# as &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt; or &lt;code&gt;Memory&amp;lt;T&amp;gt;&lt;/code&gt; without copying. DLPack tensor exchange goes further: you can share GPU tensors with PyTorch, JAX, and TensorFlow without any host-side copy at all, even when the data lives on CUDA. pythonnet doesn't do either of these.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A real async bridge.&lt;/strong&gt; Python's &lt;code&gt;asyncio&lt;/code&gt; coroutines become C# &lt;code&gt;Task&lt;/code&gt;s. Python async generators become &lt;code&gt;IAsyncEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;. &lt;code&gt;CancellationToken&lt;/code&gt; works. &lt;code&gt;Task.WhenAll&lt;/code&gt; works. If you're writing &lt;code&gt;await&lt;/code&gt; in C# today, calling Python's async world feels like a first-class citizen, not a workaround.&lt;/p&gt;

&lt;p&gt;The call latency difference is also worth knowing: PyDotNet benchmarks at roughly 1–3 µs per call, compared to 5–20 µs for pythonnet and 1–50 ms for the subprocess approach.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Call latency&lt;/th&gt;
&lt;th&gt;Zero-copy memory&lt;/th&gt;
&lt;th&gt;Async coroutines&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PyDotNet&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~1–3 µs&lt;/td&gt;
&lt;td&gt;✓ &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt; / DLPack&lt;/td&gt;
&lt;td&gt;✓ native &lt;code&gt;Task&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pythonnet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~5–20 µs&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subprocess&lt;/td&gt;
&lt;td&gt;~1–50 ms&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REST / gRPC&lt;/td&gt;
&lt;td&gt;~0.5–10 ms&lt;/td&gt;
&lt;td&gt;✗&lt;/td&gt;
&lt;td&gt;via HTTP/2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The async ecosystem problem
&lt;/h2&gt;

&lt;p&gt;Python has quietly built one of the richest async ecosystems in any language. Whether it's &lt;code&gt;asyncio&lt;/code&gt;, &lt;code&gt;httpx&lt;/code&gt;, LangChain's streaming APIs, or virtually every modern data pipeline library — Python's &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; model is everywhere. The problem is that most of these libraries were designed to live inside a Python event loop, not to be called from a .NET process.&lt;/p&gt;

&lt;p&gt;PyDotNet's async bridge handles this. Python coroutines look like ordinary .NET &lt;code&gt;Task&lt;/code&gt;s, and Python async generators look like &lt;code&gt;IAsyncEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;. If you're already writing &lt;code&gt;await&lt;/code&gt; in C#, consuming Python's async code feels surprisingly natural.&lt;/p&gt;

&lt;p&gt;This article walks through the full picture: from the simplest coroutine call to producer/consumer queues, structured concurrency, and proper cancellation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The simplest case: awaiting a coroutine
&lt;/h2&gt;

&lt;p&gt;Say you have a Python function that fetches data asynchronously. It doesn't matter what it actually does — it could call &lt;code&gt;httpx&lt;/code&gt;, query a database, or just &lt;code&gt;await asyncio.sleep(...)&lt;/code&gt; for demonstration. From C#, you call it with &lt;code&gt;CallAsync&amp;lt;T&amp;gt;()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;interp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
&lt;/span&gt;    &lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;slow_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="s"&gt;""");
&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;interp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ImportModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;slowAdd&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"slow_add"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;slowAdd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="m"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// result == 42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The coroutine runs on a .NET thread-pool thread using its own &lt;code&gt;asyncio&lt;/code&gt; event loop, and the result comes back as a proper &lt;code&gt;Task&amp;lt;int&amp;gt;&lt;/code&gt;. The C# &lt;code&gt;await&lt;/code&gt; suspends the calling context without blocking the thread, exactly as you'd expect.&lt;/p&gt;

&lt;p&gt;If you don't need the return value — a fire-and-forget log write, for example — there's a non-generic overload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"System started successfully"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Running coroutines in parallel
&lt;/h2&gt;

&lt;p&gt;Because &lt;code&gt;CallAsync&amp;lt;T&amp;gt;()&lt;/code&gt; returns a real &lt;code&gt;Task&amp;lt;T&amp;gt;&lt;/code&gt;, you can feed it straight into &lt;code&gt;Task.WhenAll&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;greet&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fetch_greeting"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"Charlie"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;greetings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three Python coroutines run concurrently. Each one gets its own event loop on a thread-pool thread, so there's no shared-loop bottleneck. For independent I/O-bound tasks — think fetching from multiple APIs, running a batch of model inferences — this is a straightforward speedup without any manual threading on your side.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keyword arguments
&lt;/h2&gt;

&lt;p&gt;Python functions often have keyword-only arguments, and async functions are no different. PyDotNet passes kwargs via an &lt;code&gt;IDictionary&amp;lt;string, object?&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"compute_stats"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;40&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"scale"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2.0&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also skip &lt;code&gt;GetFunction&lt;/code&gt; entirely and call by name through &lt;code&gt;module.CallAsync(...)&lt;/code&gt; when you don't need to hold a reference to the function object.&lt;/p&gt;




&lt;h2&gt;
  
  
  Async generators as &lt;code&gt;IAsyncEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is where it gets genuinely useful. Python's async generators — functions that &lt;code&gt;yield&lt;/code&gt; inside &lt;code&gt;async def&lt;/code&gt; — map directly to C#'s &lt;code&gt;IAsyncEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;, which means you can iterate them with &lt;code&gt;await foreach&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;interp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
&lt;/span&gt;    &lt;span class="n"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fibonacci_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;
            &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="s"&gt;""");
&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"fibonacci_stream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; "&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// 0 1 1 2 3 5 8 13&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each value is pulled lazily. The Python generator suspends at each &lt;code&gt;yield&lt;/code&gt;, and the C# side gets a &lt;code&gt;ValueTask&amp;lt;bool&amp;gt;&lt;/code&gt; for each &lt;code&gt;MoveNextAsync&lt;/code&gt;. You're not materialising the whole sequence upfront — if the generator produces a continuous feed (prices, sensor readings, log events), your C# code processes items as they arrive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kwargs in async generators
&lt;/h3&gt;

&lt;p&gt;For generators that take keyword arguments, use the overload that accepts both positional and keyword arguments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rangeStream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"range_stream"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rangeStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"stop"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"step"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; "&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// 2 5 8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Early exit and resource cleanup
&lt;/h2&gt;

&lt;p&gt;One thing that can trip you up with async generators is the &lt;code&gt;finally&lt;/code&gt; block. If your Python generator holds a resource — a file, a connection, a lock — it's supposed to release it in &lt;code&gt;finally&lt;/code&gt;, which runs when the generator is closed. But if you &lt;code&gt;break&lt;/code&gt; out of the loop early, Python needs to be explicitly told to close the generator by calling &lt;code&gt;aclose()&lt;/code&gt; on it.&lt;/p&gt;

&lt;p&gt;PyDotNet handles this automatically. When you break out of an &lt;code&gt;await foreach&lt;/code&gt; loop, &lt;code&gt;DisposeAsync()&lt;/code&gt; on the enumerator is called, which calls Python's &lt;code&gt;aclose()&lt;/code&gt; coroutine. The generator's &lt;code&gt;finally&lt;/code&gt; block runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resource_stream&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;This&lt;/span&gt; &lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="n"&gt;even&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;side&lt;/span&gt; &lt;span class="n"&gt;breaks&lt;/span&gt; &lt;span class="n"&gt;early&lt;/span&gt;
        &lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resourceStream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallAsyncEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;())&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// aclose() fires, generator finally-block runs&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the generator exhausts naturally, &lt;code&gt;aclose()&lt;/code&gt; is skipped — &lt;code&gt;StopAsyncIteration&lt;/code&gt; already means there's nothing to close.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;CancellationToken&lt;/code&gt; support
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CallAsync&amp;lt;T&amp;gt;&lt;/code&gt; accepts a &lt;code&gt;CancellationToken&lt;/code&gt; for timeout and cancellation scenarios:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Already cancelled before the call — throws immediately&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;slowValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="m"&gt;21&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OperationCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// thrown without ever calling into Python&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Timeout-based cancellation&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;slowValue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="m"&gt;21&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When cancellation fires, the returned &lt;code&gt;Task&lt;/code&gt; transitions to &lt;code&gt;Canceled&lt;/code&gt;. The Python coroutine may still complete on the thread-pool thread it was running on — there's no way to interrupt CPython mid-execution from the outside — but the C# caller gets the cancellation exception promptly.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;PyAsyncQueue&amp;lt;T&amp;gt;&lt;/code&gt;: producer/consumer across the boundary
&lt;/h2&gt;

&lt;p&gt;Sometimes you want a genuine producer/consumer setup where both the .NET side and the Python side are independently active. &lt;code&gt;PyAsyncQueue&amp;lt;T&amp;gt;&lt;/code&gt; wraps Python's &lt;code&gt;asyncio.Queue&lt;/code&gt; and exposes it as a .NET queue with &lt;code&gt;PutAsync&lt;/code&gt; / &lt;code&gt;GetAsync&lt;/code&gt; / &lt;code&gt;ReadAllAsync&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PyAsyncQueue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;producer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"alpha"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"beta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"gamma"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"delta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"epsilon"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PutAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAllAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WhenAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ContinueWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The queue lives on the Python side (&lt;code&gt;asyncio.Queue&lt;/code&gt;), and both .NET tasks interact with it through PyDotNet's GIL management. You can optionally pass a &lt;code&gt;maxsize&lt;/code&gt; to &lt;code&gt;Create(interp, maxsize: 10)&lt;/code&gt; to get backpressure: &lt;code&gt;PutAsync&lt;/code&gt; will block until space is available.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;PyTaskGroup&lt;/code&gt;: concurrent coroutines with a shared result set
&lt;/h2&gt;

&lt;p&gt;When you want to run a batch of coroutines and collect their results together, &lt;code&gt;PyTaskGroup&lt;/code&gt; is cleaner than manually managing a list of tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;computeFunc&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"compute"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="k"&gt;group&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PyTaskGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;computeFunc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;computeFunc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;computeFunc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// [9, 16, 25]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood it uses &lt;code&gt;asyncio.gather()&lt;/code&gt;, so all three coroutines run on the same event loop iteration — proper cooperative concurrency, not separate threads. On Python 3.11+ you can also use &lt;code&gt;RunWithTaskGroupAsync()&lt;/code&gt; which routes through &lt;code&gt;asyncio.TaskGroup&lt;/code&gt; for structured concurrency semantics (exceptions propagate correctly from any member of the group).&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;EvaluateAsync&amp;lt;T&amp;gt;&lt;/code&gt;: driving a coroutine you already have
&lt;/h2&gt;

&lt;p&gt;Sometimes Python code creates a coroutine object before you can call it from C#. &lt;code&gt;EvaluateAsync&amp;lt;T&amp;gt;&lt;/code&gt; on the interpreter handles this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;interp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
&lt;/span&gt;    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;async_pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt; &lt;span class="p"&gt;**&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;

    &lt;span class="n"&gt;_pending_coro&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;async_pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="s"&gt;""");
&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;interp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EvaluateAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"_pending_coro"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 59049&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The string is evaluated in Python's &lt;code&gt;__main__&lt;/code&gt; scope, the result is treated as a coroutine object, and it's driven to completion on a thread-pool event loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it actually works
&lt;/h2&gt;

&lt;p&gt;A few implementation details are worth knowing.&lt;/p&gt;

&lt;p&gt;Each &lt;code&gt;CallAsync&amp;lt;T&amp;gt;&lt;/code&gt; call runs the coroutine on a thread-pool thread using a &lt;strong&gt;dedicated &lt;code&gt;asyncio&lt;/code&gt; event loop&lt;/strong&gt; — &lt;code&gt;asyncio.new_event_loop()&lt;/code&gt;, &lt;code&gt;loop.run_until_complete(coro)&lt;/code&gt;, &lt;code&gt;loop.close()&lt;/code&gt;. This means coroutines that use &lt;code&gt;asyncio.sleep&lt;/code&gt;, &lt;code&gt;asyncio.gather&lt;/code&gt;, or any standard asyncio primitive work correctly. It also means there's no shared event loop that could become a bottleneck.&lt;/p&gt;

&lt;p&gt;The GIL is acquired by the thread-pool thread before any Python call and released after. The calling C# thread never holds the GIL, so .NET's thread pool keeps running normally while Python executes.&lt;/p&gt;

&lt;p&gt;For async generators, each &lt;code&gt;MoveNextAsync()&lt;/code&gt; dispatches to a thread-pool thread that acquires the GIL, calls &lt;code&gt;__anext__()&lt;/code&gt; on the Python async iterator, drives it to the next &lt;code&gt;yield&lt;/code&gt; using a mini event loop, and returns the value. The iterator object is owned by the &lt;code&gt;IAsyncEnumerator&amp;lt;T&amp;gt;&lt;/code&gt; implementation and released (with &lt;code&gt;aclose()&lt;/code&gt; if needed) in &lt;code&gt;DisposeAsync&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;If your application already does &lt;code&gt;await&lt;/code&gt; in C#, PyDotNet's async bridge is the path of least resistance to Python's async ecosystem. You don't need to redesign your architecture, spin up a sidecar process, or learn a new IPC protocol. The Python coroutine looks like a &lt;code&gt;Task&lt;/code&gt;. The Python async generator looks like &lt;code&gt;IAsyncEnumerable&amp;lt;T&amp;gt;&lt;/code&gt;. Cancellation works with the tokens you already have.&lt;/p&gt;

&lt;p&gt;The library is on NuGet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet add package PyDotNet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python 3.11–3.14, .NET 8/9/10, Windows/Linux/macOS, x64 and ARM64. The async features don't require any optional packages — they're in the core library.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/zcsizmadia/PyDotNet/tree/main/samples" rel="noopener noreferrer"&gt;sample projects&lt;/a&gt; have runnable examples for every scenario covered here.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>csharp</category>
      <category>python</category>
      <category>datascience</category>
    </item>
  </channel>
</rss>
