Pyinfra: Automate Infrastructure Using Python

https://pyinfra.com/

automate
thousands of
servers

pyinfra is a python-native, agentless automation tool that runs commands over ssh — concurrently, idempotently, and 6× faster than ansible.

$ uv tool install pyinfra

[mit license] · [python 3.10+] · [no agents] · [zero config]

 1
 2 
 3from pyinfra.operations import apt, files, systemd
 4 
 5apt.packages(
 6    packages=["nginx", "certbot"],
 7    update=True,
 8)
 9 
10files.template(
11    src="templates/nginx.conf.j2",
12    dest="/etc/nginx/sites-enabled/api",
13)
14 
15systemd.service("nginx", reloaded=True)
 1
 2 
 3web = [
 4    ("web-01.prod", {"role": "edge"}),
 5    ("web-02.prod", {"role": "edge"}),
 6    *[(f"web-{i:02}.prod", {}) for i in range(3, 24)],
 7]
 8 
 9db = [
10    ("db-01.prod", {"role": "primary"}),
11    ("db-02.prod", {"role": "replica"}),
12]
13 
14
15
$ pyinfra inventory.py deploy.py --limit web
 
--> Loading inventory…
    Hosts: web-01..web-23
 
--> Gathering facts (concurrent)…
    23 hosts · 0.6s
 
--> Running deploy.py…
    ✓ web-01.prod   3 ops   changed=2   0.42s
    ✓ web-02.prod   3 ops   changed=2   0.39s
    ⟳ web-03.prod   running…  apt.packages
 
--> Summary
    successful: 23   changed: 18   failed: 0   total: 2.1s

NORMAL deploy.py python 23 hosts ready · --dry · 17:42

// streaming output

See what changes,
before it changes.

Run with --dry for a per-host diff of every operation pyinfra would perform. Run for real and watch results stream back in parallel.

bash · zsh · ~/ops live

$ pyinfra inventory.py deploy.py --limit web

--> Loading inventory…

Hosts: web-01..web-24, db-01..db-04

--> Gathering facts (concurrent)…

24 hosts · 0.6s

--> Running deploy.py…

✓ web-01.prod 3 ops changed=2 0.42s ✓ web-02.prod 3 ops changed=2 0.39s ✓ web-03.prod 3 ops changed=0 0.18s ✓ web-04.prod 3 ops changed=2 0.44s ⟳ web-05.prod running… apt.packages … 19 more

--> Summary

successful: 24 changed: 18 no-change: 6 failed: 0 total: 2.1s

// features

Why pyinfra in six points.

def pure():

Just Python

No yaml. No jinja-in-yaml. Real control flow. Your editor already understands it.

def fast():

Concurrent ssh

6× faster than ansible on identical workloads. Built on gevent + SSH.

def safe():

Diff before apply

Run --dry to preview every change. Operations are idempotent — re-runs are no-ops.

def small():

0 agents

Only requirement on hosts: a shell and ssh. No daemons. No state files. No control plane.

def big():

Scale-ready

Works on 1 host or 10,000. Parallel execution, realtime streaming output.

def open():

Hackable

Custom operation in 10 lines. Connect to anything that speaks a shell — docker, lxc, k8s.

// vs ansible

$ diff ansible/ pyinfra/

--- ansible/playbook.yml 16 lines

- hosts: web
  tasks:
    - name: install nginx
      apt:
        name: nginx
        update_cache: yes
    - name: render config
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/sites-enabled/api
      notify: reload nginx
  handlers:
    - name: reload nginx
      service:
        name: nginx
        state: reloaded

+++ pyinfra/deploy.py 8 lines

from pyinfra.operations import apt, files, systemd
apt.packages(["nginx"], update=True)
cfg = files.template(
    src="nginx.conf.j2",
    dest="/etc/nginx/sites-enabled/api",
)
if cfg.will_change:
    systemd.service("nginx", reloaded=True)

// manifesto

#01

Code > config

A loop is just a loop. Stop encoding control flow into yaml.

#02

Show, then do

Diff first. Apply second. Surprise nobody.

#03

Stay out of the way

No agent. No state file. No control plane. SSH and go.

#04

Read like english

Operations are nouns and verbs. apt.packages. files.template. systemd.service.

// 180+ contributors

Thank you to hundreds open source contributors from companies and institutes all over...

SAP

EPAM Systems

Lawrence Livermore

Utrecht University

Odoo

Rochester Inst. of Tech.

Linköping University

Paul Scherrer Institute

Iress

NPR

Fox-IT

Prezi

Sensorfact

EDITED

Cynerio

# ready when you are

$ uv tool install pyinfra_

Read the 5-minute quickstart. Deploy your first host today. Replace your ansible repo next quarter.

{
"by": "InitEnabler",
"descendants": 217,
"id": 40211655,
"kids": [
40212701,
40212449,
40215908,
40213558,
40212747,
40214789,
40212984,
40215385,
40218145,
40212897,
40214563,
40213946,
40218169,
40228934,
40213688,
40212026,
40221021,
40213530,
40232291,
40220186,
40213062,
40213061,
40221616,
40215062,
40219627,
40215879,
40220525,
40213268,
40212008,
40219060,
40212023,
40212761,
40221893,
40212588,
40214189,
40214753,
40213412,
40222378
],
"score": 644,
"time": 1714488885,
"title": "Pyinfra: Automate Infrastructure Using Python",
"type": "story",
"url": "https://pyinfra.com/"
}
{
"author": "pyinfra",
"date": null,
"description": "pyinfra runs commands over SSH against thousands of hosts in seconds. No DSL. No YAML. No agents. Just Python.",
"image": "https://pyinfra.com/static/logo_readme.png",
"logo": null,
"publisher": "pyinfra",
"title": "pyinfra — agentless infrastructure automation, in plain Python",
"url": "https://pyinfra.com/"
}
{
"url": "https://pyinfra.com/",
"title": "pyinfra — agentless infrastructure automation, in plain Python",
"description": "automatethousands ofservers pyinfra is a python-native, agentless automation tool that runs commands over ssh — concurrently, idempotently, and 6× faster than...",
"links": [
"https://pyinfra.com/"
],
"image": "https://pyinfra.com/static/logo_readme.png",
"content": "<div>\n <div>\n <h2>automate<br /><span>thousands</span> of<br />servers</h2>\n <p>\n pyinfra is a python-native, agentless automation tool that runs\n commands over ssh — concurrently, idempotently, and\n <strong>6× faster than ansible</strong>.\n </p>\n <p><span>$</span>\n <span>uv tool install pyinfra</span>\n </p>\n <p><span>[mit license]</span>\n <span>·</span>\n <span>[python 3.10+]</span>\n <span>·</span>\n <span>[no agents]</span>\n <span>·</span>\n <span>[zero config]</span>\n </p>\n </div>\n <div>\n <pre>\n<span><span> 1</span></span>\n<span><span> 2</span> </span>\n<span><span> 3</span><span>from</span> <span>pyinfra.operations</span> <span>import</span> apt, files, systemd</span>\n<span><span> 4</span> </span>\n<span><span> 5</span><span>apt.packages</span>(</span>\n<span><span> 6</span> packages=[<span>\"nginx\"</span>, <span>\"certbot\"</span>],</span>\n<span><span> 7</span> update=<span>True</span>,</span>\n<span><span> 8</span>)</span>\n<span><span> 9</span> </span>\n<span><span>10</span><span>files.template</span>(</span>\n<span><span>11</span> src=<span>\"templates/nginx.conf.j2\"</span>,</span>\n<span><span>12</span> dest=<span>\"/etc/nginx/sites-enabled/api\"</span>,</span>\n<span><span>13</span>)</span>\n<span><span>14</span> </span>\n<span><span>15</span><span>systemd.service</span>(<span>\"nginx\"</span>, reloaded=<span>True</span>)</span>\n</pre>\n <pre>\n<span><span> 1</span></span>\n<span><span> 2</span> </span>\n<span><span> 3</span>web = [</span>\n<span><span> 4</span> (<span>\"web-01.prod\"</span>, {<span>\"role\"</span>: <span>\"edge\"</span>}),</span>\n<span><span> 5</span> (<span>\"web-02.prod\"</span>, {<span>\"role\"</span>: <span>\"edge\"</span>}),</span>\n<span><span> 6</span> *[(<span>f</span><span>\"web-{i:02}.prod\"</span>, {}) <span>for</span> i <span>in</span> <span>range</span>(3, 24)],</span>\n<span><span> 7</span>]</span>\n<span><span> 8</span> </span>\n<span><span> 9</span>db = [</span>\n<span><span>10</span> (<span>\"db-01.prod\"</span>, {<span>\"role\"</span>: <span>\"primary\"</span>}),</span>\n<span><span>11</span> (<span>\"db-02.prod\"</span>, {<span>\"role\"</span>: <span>\"replica\"</span>}),</span>\n<span><span>12</span>]</span>\n<span><span>13</span> </span>\n<span><span>14</span></span>\n<span><span>15</span></span>\n</pre>\n <pre>\n<span><span>$ pyinfra inventory.py deploy.py --limit web</span></span>\n<span> </span>\n<span>--&gt; Loading inventory…</span>\n<span><span> Hosts: web-01..web-23</span></span>\n<span> </span>\n<span>--&gt; Gathering facts (concurrent)…</span>\n<span><span> 23 hosts · 0.6s</span></span>\n<span> </span>\n<span>--&gt; Running deploy.py…</span>\n<span><span> ✓ web-01.prod 3 ops changed=2 0.42s</span></span>\n<span><span> ✓ web-02.prod 3 ops changed=2 0.39s</span></span>\n<span><span> ⟳ web-03.prod running… apt.packages</span></span>\n<span> </span>\n<span>--&gt; Summary</span>\n<span><span> successful: 23 changed: 18 failed: 0 total: 2.1s</span></span>\n</pre>\n <p><span>NORMAL deploy.py python</span>\n <span>23 hosts ready · --dry · 17:42</span>\n </p>\n </div>\n </div>\n<div>\n <div>\n <p>// streaming output</p>\n <h2>See what changes,<br /><span>before it changes.</span></h2>\n <p>\n Run with <strong>--dry</strong> for a per-host diff of every operation pyinfra\n would perform. Run for real and watch results stream back in parallel.\n </p>\n </div>\n <div>\n <p><span>bash · zsh · ~/ops</span>\n <span><span>●</span> live</span>\n </p>\n <pre>\n<span>$ pyinfra inventory.py deploy.py --limit web</span>\n<p>--&gt; Loading inventory…</p>\n<span> Hosts: web-01..web-24, db-01..db-04</span>\n<p>--&gt; Gathering facts (concurrent)…</p>\n<span> 24 hosts · 0.6s</span>\n<p>--&gt; Running deploy.py…</p>\n<span> ✓ web-01.prod 3 ops changed=2 0.42s</span>\n<span> ✓ web-02.prod 3 ops changed=2 0.39s</span>\n<span> ✓ web-03.prod 3 ops changed=0 0.18s</span>\n<span> ✓ web-04.prod 3 ops changed=2 0.44s</span>\n<span> ⟳ web-05.prod running… apt.packages</span>\n<span> … 19 more</span>\n<p>--&gt; Summary</p>\n<span> successful: 24 changed: 18 no-change: 6 failed: 0</span>\n<span> total: 2.1s</span>\n</pre>\n </div>\n </div>\n<div>\n <div>\n <p>// features</p>\n <h2>Why pyinfra <span>in six points.</span></h2>\n </div>\n <div>\n <article>\n <p><span>def</span> <span>pure</span>():</p>\n <h3>Just Python</h3>\n <p>No yaml. No jinja-in-yaml. Real control flow. Your editor already understands it.</p>\n </article>\n <article>\n <p><span>def</span> <span>fast</span>():</p>\n <h3>Concurrent ssh</h3>\n <p>6× faster than ansible on identical workloads. Built on gevent + SSH.</p>\n </article>\n <article>\n <p><span>def</span> <span>safe</span>():</p>\n <h3>Diff before apply</h3>\n <p>Run --dry to preview every change. Operations are idempotent — re-runs are no-ops.</p>\n </article>\n <article>\n <p><span>def</span> <span>small</span>():</p>\n <h3>0 agents</h3>\n <p>Only requirement on hosts: a shell and ssh. No daemons. No state files. No control plane.</p>\n </article>\n <article>\n <p><span>def</span> <span>big</span>():</p>\n <h3>Scale-ready</h3>\n <p>Works on 1 host or 10,000. Parallel execution, realtime streaming output.</p>\n </article>\n <article>\n <p><span>def</span> <span>open</span>():</p>\n <h3>Hackable</h3>\n <p>Custom operation in 10 lines. Connect to anything that speaks a shell — docker, lxc, k8s.</p>\n </article>\n </div>\n </div>\n<div>\n <p>// vs ansible</p>\n <h2>$ diff ansible/ pyinfra/</h2>\n <div>\n <div>\n <p><span>--- ansible/playbook.yml</span>\n <span>16 lines</span>\n </p>\n<pre>- hosts: web\n tasks:\n - name: install nginx\n apt:\n name: nginx\n update_cache: yes\n - name: render config\n template:\n src: nginx.conf.j2\n dest: /etc/nginx/sites-enabled/api\n notify: reload nginx\n handlers:\n - name: reload nginx\n service:\n name: nginx\n state: reloaded</pre>\n </div>\n <div>\n <p><span>+++ pyinfra/deploy.py</span>\n <span>8 lines</span>\n </p>\n<pre><span>from</span> <span>pyinfra.operations</span> <span>import</span> apt, files, systemd\n<span>apt.packages</span>([<span>\"nginx\"</span>], update=<span>True</span>)\ncfg = <span>files.template</span>(\n src=<span>\"nginx.conf.j2\"</span>,\n dest=<span>\"/etc/nginx/sites-enabled/api\"</span>,\n)\n<span>if</span> cfg.will_change:\n <span>systemd.service</span>(<span>\"nginx\"</span>, reloaded=<span>True</span>)</pre>\n </div>\n </div>\n </div>\n<div>\n <p>// manifesto</p>\n <div>\n <div>\n <p>#01</p>\n <p>Code &gt; config</p>\n <p>A loop is just a loop. Stop encoding control flow into yaml.</p>\n </div>\n <div>\n <p>#02</p>\n <p>Show, then do</p>\n <p>Diff first. Apply second. Surprise nobody.</p>\n </div>\n <div>\n <p>#03</p>\n <p>Stay out of the way</p>\n <p>No agent. No state file. No control plane. SSH and go.</p>\n </div>\n <div>\n <p>#04</p>\n <p>Read like english</p>\n <p>Operations are nouns and verbs. apt.packages. files.template. systemd.service.</p>\n </div>\n </div>\n </div>\n<div>\n <p>// 180+ contributors</p>\n <p>\n Thank you to hundreds open source contributors from companies and institutes all over...\n </p>\n <div>\n <p>SAP</p>\n <p>EPAM Systems</p>\n <p>Lawrence Livermore</p>\n <p>Utrecht University</p>\n <p>Odoo</p>\n <p>Rochester Inst. of Tech.</p>\n <p>Linköping University</p>\n <p>Paul Scherrer Institute</p>\n <p>Iress</p>\n <p>NPR</p>\n <p>Fox-IT</p>\n <p>Prezi</p>\n <p>Sensorfact</p>\n <p>EDITED</p>\n <p>Cynerio</p>\n </div>\n </div>\n<div>\n <p># ready when you are</p>\n <p><img src=\"https://pyinfra.com/static/logo.png\" /></p>\n <h2><span>$ </span>uv tool install pyinfra<span>_</span></h2>\n <p>Read the 5-minute quickstart. Deploy your first host today. Replace your ansible repo next quarter.</p>\n </div>",
"author": "pyinfra",
"favicon": "https://pyinfra.com/static/logo.png",
"source": "pyinfra.com",
"published": "",
"ttr": 113,
"type": "website"
}