Skip to main content
Operations are the core building blocks of pyinfra. They define the desired state of your infrastructure and pyinfra ensures that state is achieved. This guide covers everything you need to know about using operations effectively.

What Are Operations?

Operations are Python functions that:
  • Define the desired state of a system (e.g., “package X should be installed”)
  • Generate shell commands to achieve that state
  • Are idempotent - running them multiple times produces the same result
  • Can detect changes before applying them (dry-run mode)

Basic Operation Usage

Importing Operations

Operations are organized by category in the pyinfra.operations module:
from pyinfra.operations import (
    apt,      # Debian/Ubuntu package management
    yum,      # RedHat/CentOS package management
    files,    # File and directory operations
    server,   # System operations
    git,      # Git operations
    docker,   # Docker operations
)

Calling Operations

Every operation call should include a descriptive name parameter:
apt.packages(
    name="Install web server packages",
    packages=["nginx", "certbot"],
    update=True,
    _sudo=True,
)
The name parameter is required and helps you identify operations in pyinfra’s output.

Common Operations

Package Management

from pyinfra.operations import apt

# Install packages
apt.packages(
    name="Install packages",
    packages=["nginx", "postgresql", "redis-server"],
    update=True,
    _sudo=True,
)

# Remove packages
apt.packages(
    name="Remove old packages",
    packages=["apache2"],
    present=False,
    _sudo=True,
)

# Upgrade all packages
apt.upgrade(
    name="Upgrade all packages",
    _sudo=True,
)

File Operations

from pyinfra.operations import files

# Upload a file
files.put(
    name="Upload application config",
    src="config/app.conf",
    dest="/etc/myapp/app.conf",
    user="www-data",
    group="www-data",
    mode="644",
    _sudo=True,
)

# Upload with templating
files.template(
    name="Upload nginx config from template",
    src="templates/nginx.conf.j2",
    dest="/etc/nginx/sites-available/myapp",
    user="root",
    mode="644",
    _sudo=True,
)

System Operations

from pyinfra.operations import server

# Create user
server.user(
    name="Create application user",
    user="appuser",
    home="/home/appuser",
    shell="/bin/bash",
    system=True,
    _sudo=True,
)

# Create group
server.group(
    name="Create application group",
    group="appgroup",
    _sudo=True,
)

Git Operations

from pyinfra.operations import git

# Clone repository
git.repo(
    name="Clone application repository",
    src="https://github.com/example/myapp.git",
    dest="/opt/myapp",
    branch="main",
    pull=True,  # Pull latest changes if already cloned
    user="deploy",
    _sudo=True,
)

Global Arguments

Global arguments (prefixed with _) control how operations are executed:

Common Global Arguments

from pyinfra.operations import apt

apt.packages(
    name="Install packages",
    packages=["nginx"],
    
    # Execute with sudo
    _sudo=True,
    
    # Execute as specific user
    _sudo_user="postgres",
    
    # Set environment variables
    _env={"DATABASE_URL": "postgres://localhost/mydb"},
    
    # Set timeout (seconds)
    _timeout=300,
    
    # Continue on error
    _continue_on_error=True,
    
    # Only run if condition is true
    _if=lambda: host.get_fact("LinuxDistribution") == "Ubuntu",
)
Global arguments are prefixed with underscore (_) to distinguish them from operation-specific parameters.

Operation Return Values

Operations return metadata that you can use for conditional execution:
from pyinfra.operations import files, server

# Capture operation result
config_changed = files.template(
    name="Upload nginx config",
    src="templates/nginx.conf.j2",
    dest="/etc/nginx/nginx.conf",
    _sudo=True,
)

# Conditionally restart if config changed
server.service(
    name="Restart nginx if config changed",
    service="nginx",
    restarted=True,
    _sudo=True,
    _if=lambda: config_changed.did_change(),
)
Use did_change() to check if an operation made changes, enabling smart service restarts and conditional operations.

Working with Templates

pyinfra uses Jinja2 for templating:
1

Create a template file

Create templates/nginx.conf.j2:
templates/nginx.conf.j2
server {
    listen {{ port }};
    server_name {{ server_name }};
    
    location / {
        proxy_pass http://{{ backend_host }}:{{ backend_port }};
    }
}
2

Use template in deploy

deploy.py
from pyinfra import host
from pyinfra.operations import files

files.template(
    name="Upload nginx config",
    src="templates/nginx.conf.j2",
    dest="/etc/nginx/sites-available/myapp",
    port=host.data.get("nginx_port", 80),
    server_name=host.data.get("server_name", "example.com"),
    backend_host="localhost",
    backend_port=8000,
    _sudo=True,
)

Combining Operations

Build complex deployments by combining multiple operations:
deploy.py
from pyinfra.operations import apt, files, git, server

# Install dependencies
apt.packages(
    name="Install system dependencies",
    packages=["python3", "python3-pip", "git", "nginx"],
    update=True,
    _sudo=True,
)

# Create application user
server.user(
    name="Create app user",
    user="myapp",
    system=True,
    home="/opt/myapp",
    _sudo=True,
)

# Clone application
git.repo(
    name="Clone application",
    src="https://github.com/example/myapp.git",
    dest="/opt/myapp/code",
    branch="main",
    pull=True,
    user="myapp",
    _sudo=True,
)

# Install Python dependencies
server.shell(
    name="Install Python dependencies",
    commands=[
        "cd /opt/myapp/code",
        "pip3 install -r requirements.txt",
    ],
    _sudo=True,
    _sudo_user="myapp",
)

# Configure nginx
files.template(
    name="Configure nginx",
    src="templates/nginx.conf.j2",
    dest="/etc/nginx/sites-available/myapp",
    _sudo=True,
)

files.link(
    name="Enable nginx site",
    path="/etc/nginx/sites-enabled/myapp",
    target="/etc/nginx/sites-available/myapp",
    _sudo=True,
)

# Restart services
server.service(
    name="Restart nginx",
    service="nginx",
    restarted=True,
    _sudo=True,
)

Operation Categories

pyinfra includes operations for many common tasks:
CategoryModulePurpose
Package Managementapt, yum, dnf, brew, apkInstall/manage packages
FilesfilesFile/directory operations, uploads, templates
SystemserverUsers, groups, services, shell commands
Version ControlgitGit repository management
ContainersdockerDocker container/image management
Databasesmysql, postgresqlDatabase operations
Init Systemssystemd, upstart, sysvinitService management
Web ServersOperations use files for configConfigure nginx, apache

Best Practices

Always name your operations - Use descriptive names that explain what the operation does:
# Good
apt.packages(
    name="Install PostgreSQL database server",
    packages=["postgresql"],
)

# Bad
apt.packages(
    name="Install pkg",
    packages=["postgresql"],
)
Use idempotent operations - Prefer state-defining operations over raw shell commands:
# Good - idempotent
files.directory(
    name="Create directory",
    path="/opt/myapp",
    _sudo=True,
)

# Bad - will fail on second run
server.shell(
    name="Create directory",
    commands=["mkdir /opt/myapp"],
    _sudo=True,
)
Check return values for conditional execution - Use operation return values to create smart workflows:
config_changed = files.template(name="Upload config", ...)

server.service(
    name="Restart only if config changed",
    service="nginx",
    restarted=True,
    _if=lambda: config_changed.did_change(),
)

Next Steps

Build docs developers (and LLMs) love