Your Infrastructure Deserves Real Code in Python, Not YAML Soup
Loïc "wowi42" Tosser
github.com/wowi42
codeberg.org/wowi42
@wowi42
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
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{{ "{{" }} inception {{ "}}" }})country: NO becomes country: falseTASK [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 }}".
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.
No Python required on target systems - just POSIX shell access!
How PyInfra Works: Two-Phase Execution Phase 1: Fact GatheringPyInfra connects to hosts and collects facts about current state:
PyInfra compares desired state with current facts:
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!def functions() that actually workfor loops without with_items gymnasticsif/else without Jinja2 spaghettiimport and pytestmypy approves [server for server in fleet if server.is_alive()]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 ArchitectureJust 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 Extendableimport 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.
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 Integrationimport 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 FasterAnsible:
PyInfra:
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
debug tasksbreakpoint() just works)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 Testsimport 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 Ansibleimport your functionsfrom my_company.infra import setup_monitoring, configure_firewall
Production Benefits importpytest"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 Pathpyinfra @local deploy.pyMany teams have successfully migrated from Ansible to pyinfra.
Production Ready ? Production Ready 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:import pyinfra can heal youresources = {
"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
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?"
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | svc-infra: Bundled FastAPI Infrastructure | 0 | 35 | 02-01-2026 |
| 2 | Pyre: New JIT Python interpreter written in Rust | 0 | 10 | 06-04-2026 |
| 3 | Claude Code from the beach: My remote coding setup with mosh, tmux and ntfy | 0 | 10 | 14-02-2026 |
| 4 | oxyde: Type-Safe, Pydantic-Centric Async ORM | 0 | 50 | 20-02-2026 |
| 5 | Free-threaded Python: past, present, and future | 0 | 26.67 | 27-06-2026 |
| 6 | PyWry: Cross-Platform Rendering Engine in Python | 0 | 26.67 | 03-05-2026 |
| 7 | Write a coding agent from first principles: better tools | 0 | 10 | 12-07-2026 |
| 8 | syrupy: The Sweeter pytest Snapshot Plugin | 0 | 10 | 03-04-2026 |
| 9 | flake8-lazy: Detect Lazy-Importable Modules in Python 3.15+ | 0 | 52.86 | 01-06-2026 |
| 10 | From Python 3.3 to today: ending 15 years of subprocess polling | 0 | 28.18 | 01-02-2026 |