<?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: Kim Jarvis</title>
    <description>The latest articles on DEV Community by Kim Jarvis (@kimbofab).</description>
    <link>https://dev.to/kimbofab</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%2F4242515%2F04f113a7-d907-45e5-8331-022627a39991.png</url>
      <title>DEV Community: Kim Jarvis</title>
      <link>https://dev.to/kimbofab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kimbofab"/>
    <language>en</language>
    <item>
      <title>Reemote: configuration management as a Python library</title>
      <dc:creator>Kim Jarvis</dc:creator>
      <pubDate>Fri, 13 Nov 2026 08:41:54 +0000</pubDate>
      <link>https://dev.to/kimbofab/reemote-configuration-management-as-a-python-library-38cc</link>
      <guid>https://dev.to/kimbofab/reemote-configuration-management-as-a-python-library-38cc</guid>
      <description>&lt;p&gt;&lt;em&gt;Ansible is a CLI. Pyinfra is a CLI. Salt is a CLI. Chef and Puppet have their own DSLs. There is no serious configuration-management library you can &lt;code&gt;import&lt;/code&gt; from Python — until now.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Python is the language of automation glue: CI, dashboards, data pipelines, deploy orchestration. But the moment you want to describe the state of a server, you leave Python behind. You write YAML for Ansible. You write Ruby for Chef. You write Puppet DSL for Puppet. Then you invoke a binary that parses, plans, and executes for you.&lt;/p&gt;

&lt;p&gt;There is no serious configuration-management &lt;strong&gt;library&lt;/strong&gt; for Python. Nothing you can &lt;code&gt;import&lt;/code&gt;. Nothing that returns typed results you can inspect. Nothing you can embed inside an application, unit-test with &lt;code&gt;pytest&lt;/code&gt;, or &lt;code&gt;await&lt;/code&gt; from an async handler. The mainstream tools are all command-line binaries wrapped around their own task language, and if you want to trigger a deploy from Python, you end up shelling out to that binary and scraping its terminal output.&lt;/p&gt;

&lt;p&gt;That is the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in under a minute (docker connector)
&lt;/h2&gt;

&lt;p&gt;Reemote ships a &lt;strong&gt;docker connector&lt;/strong&gt; that runs your deploy inside a local container, so you do not need a remote host or an SSH key to see it work. &lt;strong&gt;You do need Docker or Podman installed locally&lt;/strong&gt; — the connector shells out to whichever CLI you name in the inventory, and there is no default. Three commands:&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="c"&gt;# 1. Install (Python 3.13+; pip works just as well as uv)&lt;/span&gt;
uv venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; uv pip install reemote

&lt;span class="c"&gt;# 2. Save the script below as hello.py, then run it&lt;/span&gt;
python hello.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# hello.py&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reemote&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;format_log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;format_plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;

&lt;span class="no"&gt;INVENTORY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"connection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"connector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"docker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"identifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"alpine:3.20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"docker_cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"podman"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# or "docker"&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="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;op&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Write a greeting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Hello from reemote!\n"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/tmp/reemote-hello.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0644"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&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="n"&gt;plans&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;INVENTORY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"== Plan =="&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plans&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"== Applied =="&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&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;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reemote starts a throwaway &lt;code&gt;alpine:3.20&lt;/code&gt; container, writes the file inside it, prints the plan and the apply log, then commits the container into a new image and removes it. Verified output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== Plan ==
demo: Write a greeting         [changed: content /tmp/reemote-hello.txt is absent, will install]
== Applied ==
demo: Write a greeting         [changed: content /tmp/reemote-hello.txt is absent, will install]
    $ scp &amp;lt;files install&amp;gt; -&amp;gt; /tmp/reemote-install-2e52d4f8373a608a/content
    $ sh -c 'mkdir -p /tmp &amp;amp;&amp;amp; cp /tmp/reemote-install-2e52d4f8373a608a/content /tmp/reemote-hello.txt &amp;amp;&amp;amp; chmod 644 /tmp/reemote-hello.txt &amp;amp;&amp;amp; rm -rf /tmp/reemote-install-2e52d4f8373a608a'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth noticing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;$ scp&lt;/code&gt; and &lt;code&gt;$ sh -c&lt;/code&gt; lines are &lt;strong&gt;command descriptors rendered canonically&lt;/strong&gt; — they look the same whether the transport is SSH, docker, or podman. Under the hood reemote is running &lt;code&gt;podman cp&lt;/code&gt; and &lt;code&gt;podman exec -i demo sh -c '...'&lt;/code&gt;; the transport wrapping is invisible in the log by design, so the same deploy reads the same on any target.&lt;/li&gt;
&lt;li&gt;Every run reports &lt;code&gt;[changed: ... is absent, will install]&lt;/code&gt;, even the second time. That is not a bug — the image workflow starts a &lt;em&gt;fresh throwaway container&lt;/em&gt; from &lt;code&gt;alpine:3.20&lt;/code&gt; on every invocation, so the file genuinely is absent each time. Point reemote at a stable target (an SSH host, or an existing container ID) and the second run correctly reports &lt;code&gt;[no change: content already up to date]&lt;/code&gt;. Idempotence is a property of the operations against a persistent state, and the quickstart deliberately trades persistence for a zero-setup demo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the whole loop. Describe the state you want, hand over an inventory, &lt;code&gt;await&lt;/code&gt; the result. No CLI, no playbook, no YAML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Python has real advantages over Ansible
&lt;/h2&gt;

&lt;p&gt;Ansible chose YAML because it is readable and it does not require knowing a programming language. That is a fair trade-off when your team does not write Python. When your team does — and most automation teams today do — the trade-off costs you more than it saves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your editor does not know your YAML.&lt;/strong&gt; No go-to-definition, no rename, no reliable type check. A typo in a module name surfaces at runtime, in production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jinja2 is a second language inside the first.&lt;/strong&gt; &lt;code&gt;{{ hostvars[...] }}&lt;/code&gt; is a stringly-typed escape hatch that fights your IDE and your test suite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roles load by convention, not by import.&lt;/strong&gt; Composition becomes a directory-layout puzzle rather than a &lt;code&gt;from ... import ...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The result is terminal output.&lt;/strong&gt; If your application needs to know what changed, you parse stdout with a regex and hope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing means testinfra, molecule, ansible-lint.&lt;/strong&gt; Not &lt;code&gt;pytest&lt;/code&gt;. You cannot &lt;code&gt;import&lt;/code&gt; a task and inspect it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Python already solves all of these. &lt;code&gt;import&lt;/code&gt; gives you composition. Types give you editor support. &lt;code&gt;pytest&lt;/code&gt; gives you tests. &lt;code&gt;await&lt;/code&gt; gives you concurrency. Data structures give you results you can consume programmatically. The reason configuration management did not use Python is that nobody shipped the library.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution: Reemote is a library
&lt;/h2&gt;

&lt;p&gt;Reemote is a Python package (&lt;code&gt;pip install reemote&lt;/code&gt;) with two properties that separate it from every CLI-shaped tool:&lt;/p&gt;

&lt;h3&gt;
  
  
  Async end to end, on asyncssh
&lt;/h3&gt;

&lt;p&gt;The transport is &lt;a href="https://asyncssh.readthedocs.io/" rel="noopener noreferrer"&gt;asyncssh&lt;/a&gt;, and the entire execution path — connecting, gathering facts, sending commands, closing — is coroutines. You &lt;code&gt;await&lt;/code&gt; a deploy and hosts progress in parallel. Within a deploy, hosts step through operations in lockstep; commands for one host run in order; network waits overlap across hosts. Your deploy script stays a plain synchronous generator, so you never write &lt;code&gt;async&lt;/code&gt; inside an operation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;plans&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;run&lt;/code&gt; is the single entry point. It validates the inventory, opens connections lazily, plans and applies in two phases, then closes everything in a &lt;code&gt;finally&lt;/code&gt;. Same code over SSH or over the docker connector — pick the transport in the inventory entry, not in the deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  A library returning structured results, not a CLI
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;run(...)&lt;/code&gt; returns &lt;code&gt;(plans, report, hosts)&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;plans&lt;/code&gt; — &lt;code&gt;{host_label: [PlannedOperation, ...]}&lt;/code&gt;, the proposed work per host.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;report&lt;/code&gt; — an &lt;code&gt;ApplyReport&lt;/code&gt; with a &lt;code&gt;LogEntry&lt;/code&gt; per applied operation, each carrying &lt;code&gt;changed&lt;/code&gt;, &lt;code&gt;reason&lt;/code&gt;, and the exact commands that were sent. &lt;code&gt;None&lt;/code&gt; when &lt;code&gt;dry=True&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hosts&lt;/code&gt; — the materialised host objects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No parsing stdout to figure out what happened. You already have the objects. &lt;code&gt;format_plan&lt;/code&gt; and &lt;code&gt;format_log&lt;/code&gt; are convenience renderers for humans; your application consumes the data directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four words to know
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inventory&lt;/strong&gt; — a list of dicts naming your targets and how to reach them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fact&lt;/strong&gt; — a read of remote state (hostname, installed packages, file contents).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operation&lt;/strong&gt; — a request to converge the server toward a described state. Operations read facts, decide whether anything must change, and yield commands only when it must.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action&lt;/strong&gt; — an immediate, non-deploy escape hatch (&lt;code&gt;Shell&lt;/code&gt;, &lt;code&gt;Script&lt;/code&gt;, &lt;code&gt;WaitForConnection&lt;/code&gt;) for one-off commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A deploy is a generator that yields operation requests in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;op&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Install nginx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"nginx"&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="n"&gt;sudo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;op&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Write landing page"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;PAGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/var/www/html/index.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0644"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sudo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;op&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;systemd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Run nginx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"nginx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;running&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sudo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every request argument is hashable, every deploy is a pure function of its host, and the runner executes it twice — once to plan, once to apply — with a determinism guard that raises if the two passes disagree. &lt;code&gt;dry=True&lt;/code&gt; stops after the plan and gives you a real preview against live facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What follows: Reemote can replace Ansible
&lt;/h2&gt;

&lt;p&gt;Once configuration management is a library, everything Python already has becomes available to your deploys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deploy from your application.&lt;/strong&gt; &lt;code&gt;await run(inventory, deploy)&lt;/code&gt; inside a FastAPI handler, a Celery task, a GitHub Action written in Python, or a REPL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compose with real code.&lt;/strong&gt; Build the inventory from a database query. Generate config text from a Pydantic model. Share validation logic between the app and the deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with &lt;code&gt;pytest&lt;/code&gt;.&lt;/strong&gt; Deploy functions are pure generators — inspect the requests they yield without touching a server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consume structured results.&lt;/strong&gt; &lt;code&gt;report.entries[i].changed&lt;/code&gt;, &lt;code&gt;.reason&lt;/code&gt;, &lt;code&gt;.commands&lt;/code&gt; are data you can log, chart, feed to a webhook, or gate a rollout on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-phase execution for free.&lt;/strong&gt; Every &lt;code&gt;run&lt;/code&gt; prepares then applies. &lt;code&gt;dry=True&lt;/code&gt; is a real preview against live state, not a simulation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same code over SSH or Docker.&lt;/strong&gt; Flip the &lt;code&gt;connector&lt;/code&gt; key and the identical deploy runs against a remote server or a local container — useful for testing a role before you point it at production, or for building an image from the same code that configures a host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handlers as ordinary Python.&lt;/strong&gt; No &lt;code&gt;notify&lt;/code&gt;/&lt;code&gt;handler&lt;/code&gt; keyword. Read the first deploy's report, freeze the hosts that changed, feed that set into a second deploy. &lt;code&gt;reemote.handlers.changed_hosts&lt;/code&gt; and &lt;code&gt;restart_handler&lt;/code&gt; ship as helpers for the common case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can use Reemote instead of Ansible whenever deployment is part of a Python program rather than a shell command in a CI job — which, for most teams building products today, is the more natural shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Reemote is not
&lt;/h2&gt;

&lt;p&gt;It is worth naming the boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not a deployment tool.&lt;/strong&gt; There is no &lt;code&gt;reemote&lt;/code&gt; command. You write the script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not an inventory format.&lt;/strong&gt; Inventory is a Python list of dicts. Load it from JSON, YAML, a database, or write it inline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a role ecosystem.&lt;/strong&gt; Reemote ships modules for &lt;code&gt;apt&lt;/code&gt;, &lt;code&gt;dnf&lt;/code&gt;, &lt;code&gt;apk&lt;/code&gt;, &lt;code&gt;files&lt;/code&gt;, &lt;code&gt;systemd&lt;/code&gt;, &lt;code&gt;server&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;mysql&lt;/code&gt;, &lt;code&gt;postgresql&lt;/code&gt;, &lt;code&gt;iptables&lt;/code&gt;, &lt;code&gt;firewalld&lt;/code&gt;, &lt;code&gt;ufw&lt;/code&gt;, &lt;code&gt;selinux&lt;/code&gt;, &lt;code&gt;lxd&lt;/code&gt;, &lt;code&gt;snap&lt;/code&gt;, &lt;code&gt;flatpak&lt;/code&gt;, &lt;code&gt;sysctl&lt;/code&gt;, &lt;code&gt;gpg&lt;/code&gt;, &lt;code&gt;openrc&lt;/code&gt;, and &lt;code&gt;hardware&lt;/code&gt; — but there is no Ansible Galaxy equivalent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a translation layer between OSes.&lt;/strong&gt; Python does not make &lt;code&gt;nginx&lt;/code&gt; installable by the same name on Alpine and Debian. You still pick the right module for the target.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it is, is the missing library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Documentation and full guide: &lt;a href="https://reemote.org/" rel="noopener noreferrer"&gt;https://reemote.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/kimjarvis/reemote" rel="noopener noreferrer"&gt;https://github.com/kimjarvis/reemote&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PyPI: &lt;code&gt;pip install reemote&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;License: EPL-2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have ever wanted to describe server state in the same language as the rest of your stack — with imports, types, tests, and &lt;code&gt;await&lt;/code&gt; — the docker quickstart at the top of this article is the whole onboarding. Give it a minute.&lt;/p&gt;

</description>
      <category>python</category>
      <category>devops</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
