def pure():
Just Python
No yaml. No jinja-in-yaml. Real control flow. Your editor already understands it.
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
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
def pure():
No yaml. No jinja-in-yaml. Real control flow. Your editor already understands it.
def fast():
6× faster than ansible on identical workloads. Built on gevent + SSH.
def safe():
Run --dry to preview every change. Operations are idempotent — re-runs are no-ops.
def small():
Only requirement on hosts: a shell and ssh. No daemons. No state files. No control plane.
def big():
Works on 1 host or 10,000. Parallel execution, realtime streaming output.
def open():
Custom operation in 10 lines. Connect to anything that speaks a shell — docker, lxc, k8s.
// vs ansible
--- 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
Read the 5-minute quickstart. Deploy your first host today. Replace your ansible repo next quarter.