A PyPlays retrospective
When I was setting up my first “production” home server, I chose to use Ansible as my IaC tool of choice. After some years of growing frustration with its YAML-based “code-but-not-really”, and while setting up a second server, I decided to do something hacky and add support for Python playbooks into Ansible, creating pyplays. Now it’s about five years from the start of pyplays and we’ve been slowly replacing its use with pyinfra in GGU infrastructure. I’d like to walk you through its creation and then ponder whether it was all worth it in the end.
Humble server beginnings
I set up my first “real” (read: operating 24/7) home server when I’d been living with my parents, back in high school. It was an SFF Dell OptiPlex 7010, primarily meant to be a NAS replacement for our old FireWire hard drive.
I knew I wanted to set it using some kind of infrastructure-as-code scripts, primarily to avoid losing track of how it is set up, with a secondary goal of being able to wipe it and start anew if any particularly ugly issues appeared. At first, I wanted to write my own tool for scripting in some language saner than bash, but that turned out to be a lot of work. Limiting myself to popular existing tools, I chose Ansible, as everything else looked too complicated for a single-server setup.
I installed Ubuntu as a host OS on the server, with LXD on top. Every service then ran in its own container. I used a single Ansible playbook to create all the containers and import roles, one per container.
This was pretty far from the (presumably) intended usecase for Ansible. I didn’t have (and still don’t have) many roles that ran on multiple machines at once. What I wanted was just a better, idempotent shell scripting, and Ansible mostly delivered.
Over time, I started growing frustrated with Ansible’s limited YAML. Having to basically code by writing an AST wouldn’t be so bad (Lisp programmers don’t mind), but restrictions like “no looping on blocks” made writing more generic roles very annoying. I never liked the “declarative” facade over what is just imperative code, but now it was starting to restrict me in practical ways.
Birth of pyplays
When setting up my second server with some friends, when I wanted to move some parts of my existing Ansible code into a common repo for both servers, I decided I’d had enough and started looking into better ways of writing playbooks.
Ansible is written in Python, so the natural choice would be some Python DSL to schedule those same Ansible modules one can use from YAML. I hooked Ansible to handle .py files by evaluating them and taking the value of some global variable to be the result of “parsing YAML”. It was ugly, having to monkey-patch long functions, but not too hard, and it worked.
This approach did come with its own idiosyncrasies. The Python code had access to many Ansible variables, so passing “parameters” was possible, but it ran before anything was actually executed, in the parsing phase. This meant that any control flow still had to be written with Ansible’s when and similar parameters. Furthermore, all arguments given to the modules were still passed through Jinja on the way. Even with all that complexity, it was better than the old YAML. Just look at this parametrised multi-user webserver role.
Death of pyplays
My co-admins were never really happy with pyplays, since it presented another layer of poorly-documented complexity on top of Ansible itself. Writing any playbook meant having the Ansible docs open and translating them into pyplays in your head. So when pyinfra appeared, we started to dream about rewriting all our IaC code in pyinfra instead and dropping Ansible, but couldn’t find the time to do that. The final push came with cheap and capable LLMs, which didn’t mind that pyplays was undocumented and that the work was a bunch of boring copy-paste. As of September 2026, most of our self-contained Ansible roles have been rewritten in pyinfra, and we’ll get to the core things like container creation Soon™.
I’m still convinced that pyplays wasn’t a bad idea – reusing Ansible’s numerous modules was far easier than starting from scratch. But a hacky add-on can’t fix things like Ansible’s execution model, and I wouldn’t have created pyplays if pyinfra was around when I first decided on Ansible.