Understand how pyinfra’s idempotent operations work and how to write predictable, repeatable infrastructure code
Idempotency is a core principle of pyinfra. An idempotent operation produces the same result when run multiple times - the second run detects that the desired state is already achieved and makes no changes. This guide explains how pyinfra achieves idempotency and how to use it effectively.
If changes are detected, pyinfra generates and executes the minimum commands needed:
# Only runs if file doesn't exist or has wrong permissionstouch /etc/myapp/config.txtchmod 644 /etc/myapp/config.txtchown root:root /etc/myapp/config.txt
Most pyinfra operations are idempotent by default:
from pyinfra.operations import apt# Idempotent - only installs if not presentapt.packages( name="Install nginx", packages=["nginx"], _sudo=True,)# Run multiple times - same result
Use operation return values to restart services only when configs change:
from pyinfra.operations import files, server# Upload config and capture resultconfig_changed = files.template( name="Upload nginx config", src="templates/nginx.conf.j2", dest="/etc/nginx/nginx.conf", _sudo=True,)# Only restart if config actually changedserver.service( name="Restart nginx if needed", service="nginx", restarted=True, _sudo=True, _if=lambda: config_changed.did_change(),)
# First run - config is different[web-01] files.template: Upload nginx config (changed)[web-01] server.service: Restart nginx# nginx was restarted because config changed
Operation return values provide metadata about execution:
from pyinfra.operations import apt, server# Capture operation resultinstall_result = apt.packages( name="Install nginx", packages=["nginx"], _sudo=True,)# Check if operation made changesif install_result.did_change(): print("Nginx was installed")# Check if operation succeededif install_result.did_succeed(): print("Installation successful")# Check if operation made no changesif install_result.did_not_change(): print("Nginx was already installed")
from pyinfra.operations import server# Only run if condition is metserver.shell( name="Initialize database", commands=["./scripts/init_db.sh"], _if=lambda: not host.get_fact(File, path="/var/lib/db/.initialized"), _sudo=True,)# Create marker file after initializationserver.shell( name="Mark database as initialized", commands=["touch /var/lib/db/.initialized"], _sudo=True,)
Facts are how pyinfra gathers information about the current state:
from pyinfra import hostfrom pyinfra.operations import apt, yum# Get OS distribution factdistro = host.get_fact("LinuxDistribution")# Use fact for conditional logicif distro in ["Ubuntu", "Debian"]: apt.packages( name="Install package", packages=["nginx"], _sudo=True, )elif distro in ["CentOS", "RedHat"]: yum.packages( name="Install package", packages=["nginx"], _sudo=True, )
Be careful when using facts that may change during execution:
from pyinfra import hostfrom pyinfra.operations import apt, filesfrom pyinfra.facts.files import File# Install nginxapt.packages( name="Install nginx", packages=["nginx"], _sudo=True,)# BAD: This fact was gathered BEFORE nginx was installed# so it will be False even though nginx is now installed!if host.get_fact(File, path="/etc/nginx/nginx.conf"): files.file( name="Remove default config", path="/etc/nginx/nginx.conf", present=False, _sudo=True, )
Always prefer using idempotent operations over manual fact checks. Operations handle state detection correctly.
File operations detect changes by comparing checksums:
from pyinfra.operations import files# Only uploads if local and remote files differfiles.put( name="Upload config file", src="config/app.conf", dest="/etc/myapp/app.conf", mode="644", _sudo=True,)
Pyinfra:
Calculates checksum of local file
Calculates checksum of remote file (if exists)
Only uploads if checksums differ or file doesn’t exist