PyInfra 🐍

Your Infrastructure Deserves Real Code in Python, Not YAML Soup 🐍

FOSDEM 2026 - Python Devroom

Loïc "wowi42" Tosser

🐙 github.com/wowi42
🌲 codeberg.org/wowi42
🦣 @wowi42

Part I: YAML Hell 🔥

Infrastructure as Code became Infrastructure as YAML

And nobody asked for this

Dante's 10th circle was just a YAML file with wrong indentation

Remember This Promise? 📜

Infrastructure as Code → Infrastructure as YAML

They lied to us

  • Code: "Human-readable programming language"
  • YAML: "Machine-readable definition file" (barely)

One is code, the other is a cry for help.

The Current State of DevOps

"Let's simplify infrastructure management!"

Proceeds to create this monstrosity:

- name: Deploy application
  hosts: all
  vars:
    deploy_state: "{{ lookup('env', 'STATE') |
                      default('present') }}"
  tasks:
    - name: Ensure application is {{ deploy_state }}
      when: ansible_os_family == "Debian" and
            not (ansible_distribution == "Ubuntu" and
            ansible_distribution_version is
            version('20.04', '>='))

Over-simplified, yet somehow overcomplicated 😭

This is what happens when you ask a config file to be a programming language

A Developer's Nightmare

  • Indentation Sensitivity → 2 spaces? 4? Tabs? Deployment cancelled.
  • String Templating Hell → Jinja2 in YAML in Jinja2 ({{ "{{" }} inception {{ "}}" }})
  • No Real Functions → Copy-paste "roles" everywhere
  • Debugging? → "ERROR: undefined variable" somewhere in 47 files
  • IDE Support? → Just vibes and prayers
  • The Norway Problemcountry: NO becomes country: false

Ansible Error Messages 🔍

TASK [Deploy app] *******************
fatal: [web01]: FAILED! => {"msg": "The task includes
an option with an undefined variable. The error was:
'dict object' has no attribute 'nonexistent'\n\nThe
error appears to be in '/playbook.yml': line 42,
column 3, but may be elsewhere in the file depending
on the exact syntax problem."}

"somewhere in the file" - Thanks, super helpful!

Add -vvvv and scroll through 1000 lines of output?

Ansible Testing 🧪

# 200 lines of YAML to test 50 lines of YAML, with Tox and Molecule
# Plus Docker, plus Vagrant, plus a PhD in pain

Yo dawg, I heard you like YAML so I put YAML in your YAML tests

# Meanwhile, in Python land:
import pytest  # That's it.

Can't use pdb. Can't breakpoint(). Just... debug: msg="{{ variable }}".

Part II: Enter PyInfra 🚀

Your Infrastructure. Actual Python.

Everything is Python (including your sanity)

>>> import antigravity  # Finally escape YAML's pull
>>> from yaml import pain
ImportError: No more. We're done here.
>>> import pyinfra
>>> # *chef's kiss*

What is PyInfra?

Python code in, shell commands out

pyinfra turns Python code into shell commands and runs them on your servers.

  • Execute ad-hoc commands
  • Write declarative operations
  • Target SSH servers, local machine, Docker containers
  • Scales from one to thousands of servers

No Python required on target systems - just POSIX shell access!

How PyInfra Works: Two-Phase Execution

Phase 1: Fact Gathering

PyInfra connects to hosts and collects facts about current state:

  • Installed packages, running services, file checksums
  • OS details, network configuration, users

Phase 2: Operation Execution

PyInfra compares desired state with current facts:

  • Generates only the necessary shell commands
  • Skips operations where state already matches
  • Executes commands in parallel across hosts

Result: Minimal changes, maximum efficiency, true idempotency

The Core Concept

# Install and configure nginx
from pyinfra.operations import apt, systemd

apt.packages(
    name="Install nginx",
    packages=["nginx"],
)

systemd.service(
    name="Restart and enable the nginx service",
    service="nginx.service",
    running=True,
    restarted=True,
    enabled=True,
)

Python in, shell commands out.

Wait... That's Just Python!

EXACTLY! 🎯

  • Real def functions() that actually work
  • Real for loops without with_items gymnastics
  • Real if/else without Jinja2 spaghetti
  • Real import and pytest
  • Type hints & autocompletemypy approves 👍
  • List comprehensions[server for server in fleet if server.is_alive()]

Declarative Operations 🔄

Only make changes when needed

PyInfra has built-in diff checking and idempotent operations

# This only runs if the package isn't installed
apt.packages(packages=["nginx"])

# This only runs if the service isn't running
systemd.service(service="nginx", running=True)

# This only runs if the file content differs
files.put(src="config.j2", dest="/etc/nginx/nginx.conf")

Declarative infrastructure with actual code

Agentless Architecture 💻

Zero dependencies on target systems

  • Deploy to any system with POSIX shell access
  • No agents to install, no daemons to manage
  • No bootstrapping required

Just SSH and shell.

Works with: Linux, macOS, BSD, containers, cloud VMs, bare metal, routers

Connectors: Target Anything 🎯

# SSH to servers (the classic)
pyinfra @ssh/web1.example.com deploy.py

# Run locally (test before you wreck)
pyinfra @local deploy.py

# Target Docker containers directly
pyinfra @docker/my-container deploy.py

# Mix and match in inventory
pyinfra inventory.py deploy.py
# inventory.py - It's just Python!
ssh_hosts = ["web1", "web2", "db1"]
docker_hosts = [("@docker/app", {"env": "staging"})]
local = ["@local"]

One tool. Every target.

Fully Extendable 📦

Leverage the entire Python package ecosystem

import requests
hosts = requests.get("https://api/hosts").json()

import boto3
instances = boto3.client('ec2').describe_instances()

from slack_sdk import WebClient
slack = WebClient(token=TOKEN)
slack.chat_postMessage(channel="#ops", text="Deploy complete!")

No custom plugins. Just pip install. 400,000+ packages on PyPI.

Embed PyInfra Anywhere 🧩

It's a library, not just a CLI

from pyinfra.api import Config, Inventory, State
from pyinfra.api.operations import run_ops
from pyinfra.operations import server

inventory = Inventory((["web1.example.com"], {}))
state = State(inventory, Config())

server.shell(commands=["echo 'Deployed from my Flask app!'"])
run_ops(state)

Infrastructure automation inside YOUR app

Seamless Integration 🔌

  • Pull inventory from Terraform, Coolify, or any API
  • Execute against Docker containers
  • Embed in Django, FastAPI, CLI tools
import requests
hosts = requests.get("https://api/hosts").json()

from datetime import datetime
if datetime.now().hour > 20:
    print("No deploys after 8 PM!")
    exit(1)

Part III: PyInfra vs Ansible

How is it better?

Let's count the ways...

Performance: Up to 10x Faster 🚀

Ansible:

  • Sequential by default
  • Heavy abstraction layers
  • Python on target required

PyInfra:

  • Efficient SSH multiplexing
  • Parallel execution built-in
  • Direct shell commands
  • Scales to thousands of servers

Up to 10x faster deployment times

Side by Side: Package Installation

Ansible (verbose YAML)

- name: Install packages
  hosts: webservers
  become: yes
  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes

    - name: Install packages
      apt:
        name: "{{ item }}"
        state: present
      with_items:
        - nginx
        - postgresql
        - redis

18 lines to install 3 packages

Side by Side: Package Installation

PyInfra (clean Python)

from pyinfra.operations import apt

apt.packages(
    packages=["nginx", "postgresql", "redis"],
    update=True,
)

6 lines vs 18 lines

Side by Side: Conditional Logic

Ansible

- name: Configure based on environment
  template:
    src: "{{ 'prod.j2' if env == 'production'
             else 'dev.j2' }}"
    dest: /etc/app/config
  when: env is defined

PyInfra

template = "prod.j2" if env == "production" else "dev.j2"
files.template(src=template, dest="/etc/app/config")

Just Python. No Jinja2 gymnastics.

Side by Side: Loops

Ansible

- name: Create users
  user:
    name: "{{ item.name }}"
    groups: "{{ item.groups }}"
  with_items:
    - { name: alice, groups: sudo }
    - { name: bob, groups: developers }
  when: item.name != 'root'

Side by Side: Loops

PyInfra

users = [
    {"name": "alice", "groups": ["sudo"]},
    {"name": "bob", "groups": ["developers"]}
]

for user in users:
    if user["name"] != "root":
        server.user(user.name, groups=user.groups)

List comprehensions > with_items

Debugging: Instant Feedback 🚨

Ansible

  • Add -vvvv flag and scroll through thousands of lines
  • Use debug tasks
  • Good luck finding the actual error

PyInfra

  • Real-time shell command output
  • Use pdb (breakpoint() just works)
  • IDE debugger support with breakpoints

Error Messages: Actual Stack Traces

Traceback (most recent call last):
  File "deploy.py", line 42, in <module>
    config["nonexistent"]
KeyError: 'nonexistent'

Exact file, line, and error. No more "may be elsewhere".

Testing: Real Unit Tests 🧪

PyInfra

import pytest
from deploy import setup_nginx

def test_nginx_config():
    config = setup_nginx("production")
    assert "ssl" in config
    assert config["worker_processes"] == 4

It's just Python. Test it like Python.

IDE Support: Modern Development

Ansible

  • Basic YAML syntax highlighting
  • Maybe some linting
  • No autocomplete, no refactoring

PyInfra

  • Full autocomplete with type hints
  • Inline documentation
  • Refactoring support (rename, extract)
  • Jump to definition

Code Reuse: Python Modules

Ansible

  • Copy-paste roles from Galaxy
  • Customize with 5 different directories
  • Fight with role dependencies

PyInfra

  • import your functions
  • Use 400,000+ PyPI packages
  • Write clean, modular code
from my_company.infra import setup_monitoring, configure_firewall

Production Benefits 📈

  • Up to 10x faster than Ansible
  • 70% less code to maintain
  • Real code reuse via import
  • 5x faster development with IDE support
  • Actual unit tests with pytest
  • Junior devs understand it - they learned Python
  • Scales to thousands of servers
  • Embeddable in your apps

But What About... 🤔

"Ansible has thousands of modules!"

PyInfra has 400,000+ Python packages. pip install requests > custom Ansible module.

"Everyone knows Ansible!"

Everyone knows Python better. And hates debugging Ansible.

"Declarative is better!"

PyInfra IS declarative. Diff checking, idempotent operations. With actual code.

But What About... (continued)

"What about dependencies on target systems?"

Zero. Agentless. Any system with POSIX shell access.

"But I need Terraform/Docker integration!"

PyInfra does both. Next question?

"Change is scary!"

So is your 5000-line playbook that nobody dares to touch.

Migration Path 🛤️

  1. Start Small - One playbook at a time
  2. Use Your IDE - Autocomplete, refactoring
  3. Leverage Python - pip packages, stdlib
  4. Test Locally - pyinfra @local deploy.py
  5. Sleep Better - No more 3 AM debugging

Many teams have successfully migrated from Ansible to pyinfra.

Production Ready ?

Production Ready 🌟

  • Battle-tested - Companies worldwide manage production with it
  • Stable APIs - Won't break with version updates
  • Comprehensive test coverage
  • Active maintenance - Regular releases
  • MIT Licensed - Open source and free forever

Use Cases

🖥️ Server Provisioning - Bare metal to production-ready
🔄 Application Deployment - Rolling updates with health checks
⚙️ Configuration Management - Keep infrastructure in sync
🔧 Ad-hoc Automation - Commands across hundreds of servers
🐳 Container Orchestration - Manage Docker containers
☁️ Cloud Infrastructure - Terraform and cloud providers

If it has SSH, PyInfra can talk to it.

Get Started Today! 🚀

pip install pyinfra  # That's the hard part. Done.
# deploy.py
from pyinfra.operations import apt, systemd

apt.packages(name="Install nginx", packages=["nginx"])

systemd.service(
    name="Start nginx",
    service="nginx.service",
    running=True,
    enabled=True,
)
pyinfra @localhost deploy.py  # *chef's kiss*

That's it. IaC with actual code.

Stop YAMLing, Start Pythoning

Find me after to discuss:

  • Your Ansible horror stories (group therapy 🛋️)
  • Migration strategies
  • Why YAML hurt you
  • How import pyinfra can heal you

Get Involved 🔗

resources = {
    "docs": "docs.pyinfra.com",
    "github": "github.com/pyinfra-dev/pyinfra",
    "pypi": "pip install pyinfra",
    "examples": "github.com/pyinfra-dev/pyinfra/tree/3.x/examples",
}

Star the repo ⭐ • Try it today • Never touch YAML again

Thank You! 🙏

return "Go forth and free your infrastructure!"

🐍 pyinfra.com → Your YAML-free future starts now

if still_writing_ansible_after_this_talk:
    raise FriendshipError("We need to talk")

See you at the Belgian beer session 🍺🧇

SCRIPT: "PyInfra isn't just a CLI - it's a library. Embed it in Django, FastAPI, anywhere."

SCRIPT: "It's just Python. Pull inventory from Terraform, add deploy freezes, block Friday deploys - all standard Python."

SCRIPT: "PyInfra versus Ansible. Let's count the ways..."

SCRIPT: "Up to 10x faster. SSH multiplexing, parallel execution, direct shell commands."

SCRIPT: "18 lines of YAML to install 3 packages."

SCRIPT: "PyInfra: 6 lines. Import, call a function, done."

SCRIPT: "Conditional logic: Jinja2 in YAML vs a ternary expression."

SCRIPT: "Loops. In Ansible: with_items, with_dict, with_file, with_nested... In Python: for loop."

SCRIPT: "In PyInfra: a for loop. The one you learned in week 1 of Python."

SCRIPT: "Debugging: -vvvv and scroll thousands of lines, or use pdb like a civilized person."

SCRIPT: "PyInfra: file, line number, exact problem. KeyError on line 42. Done."

SCRIPT: "Testing: import pytest, write tests. No molecule, no test kitchen."

SCRIPT: "IDE support: YAML highlighting vs full autocomplete, refactoring, jump to definition."

SCRIPT: "Code reuse: copy roles from Galaxy or just import a module."

SCRIPT: "10x faster. 70% less code. Junior devs understand it because they learned Python."

SCRIPT: "Objections: Ansible has modules? PyInfra has 400k packages. Everyone knows Ansible? Everyone hates debugging it."

SCRIPT: "Zero target dependencies. Terraform and Docker integration. Change is scary - so is that 5000-line playbook."

SCRIPT: "Start small. Pick one playbook. Convert it. Feel the joy."

SCRIPT: "Production ready. Stable APIs. Active maintenance. MIT licensed."

SCRIPT: "Production ready. Stable APIs. Active maintenance. MIT licensed."

SCRIPT: "Use cases: server provisioning, deployment, configuration, containers. If it has SSH, PyInfra can talk to it."

SCRIPT: "pip install pyinfra. Write deploy.py. Run it. Done. Try it during Q&A."

SCRIPT: "Stop YAMLing. Start Pythoning. Find me after for group therapy."

SCRIPT: "Star the repo. Try it today. Never touch YAML again."

SCRIPT: "Thank you! Go forth and free your infrastructure! See you at the Belgian beer session. Des questions?"