Skip to main content
RunBook Academy

AnsibleXLVI · Python and Interpreter DiscoveryRemote Python and interpreter discovery

Pinning the interpreter as a production decision

Intermediate⏱ ~20 minansible-core

What you'll learn

  • Decide between discovery and an explicit pin for a given host family
  • Place ansible_python_interpreter at the layer that matches what the value describes
  • Point a specific play at a virtualenv that carries a module dependency, without pinning the whole host
  • Detect a fleet where supposedly identical hosts have diverged on interpreter
  • Explain why a pin is part of reproducibility rather than a workaround

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Discovery is a reasonable default for a tool that has to work on a host it knows nothing about. It is not a reasonable long-term arrangement for an estate you are responsible for, and the difference is not aesthetic.

A discovered interpreter is a value that is chosen fresh on every run, by a rule that prefers whatever is newest, from a host whose package list is maintained by somebody else. That is a dependency you inherit and cannot see. A pinned interpreter is a value in Git that changes when somebody proposes a change.

The question this lesson answers is not whether to pin — on a long-lived estate you eventually will — but where the value belongs, which is a harder question than it looks.

The three things a pin can mean

ansible_python_interpreter is one variable, and teams use it to say three quite different things. Getting them confused is how a pin becomes its own problem.

“This host family uses this system interpreter.” A statement about the platform. Belongs in group_vars for the platform group, changes at distribution upgrade, and is the common case.

“This host is unusual.” A statement about one machine — an appliance with Python somewhere odd, a legacy box, a host mid-migration. Belongs in host_vars, and should carry a comment saying when it can be removed.

“This automation needs a specific environment.” A statement about the play, not the host: a module that needs a library you installed into a virtualenv. Belongs on the play or the task, not in inventory, because the host is not fundamentally a “python3.12 host” — it just has a virtualenv that this one play cares about.

The ordinary case: per-group system interpreter

Most estates need exactly this, and it takes four lines.

# inventory/production/group_vars/ubuntu_2404.yml
ansible_python_interpreter: /usr/bin/python3

# inventory/production/group_vars/rocky_9.yml
ansible_python_interpreter: /usr/bin/python3

Two groups, same value, and both lines still earn their place — because the reason is per-group even where the value is not, and because the groups will diverge eventually.

The value is /usr/bin/python3 rather than /usr/bin/python3.12 deliberately. On Debian, Ubuntu and RHEL-family systems that path is a distribution-managed symlink pointing at the interpreter the distribution’s own packages are built for — which is the interpreter that has python3-apt or python3-rpm. Pinning to it tracks the distribution’s intention and ignores any newer Python installed alongside.

A versioned path is more precise and becomes wrong at the next distribution upgrade. Reserve it for hosts where you genuinely need one specific interpreter and will notice when it disappears.

Read-only / Safe

Detecting a fleet that has already diverged

Before you pin, find out what you are pinning away from. A group whose hosts are supposed to be identical is the most valuable thing to check, because divergence there is both most surprising and most damaging.

Read-only / Safethe interpreter every host in a group actually chose
$ ansible webservers -m debug -a 'var=ansible_facts.discovered_interpreter_python' --one-line
web-a1.example.com | SUCCESS => {"ansible_facts.discovered_interpreter_python": "/usr/bin/python3.12"}
web-a2.example.com | SUCCESS => {"ansible_facts.discovered_interpreter_python": "/usr/bin/python3.12"}
web-a3.example.com | SUCCESS => {"ansible_facts.discovered_interpreter_python": "/usr/bin/python3.13"}
web-a4.example.com | SUCCESS => {"ansible_facts.discovered_interpreter_python": "/usr/bin/python3.12"}

Illustrative output

Four hosts built from the same image, and one of them is running modules under a different interpreter. Nothing has failed. Nothing will fail until a play touches apt on web-a3, at which point it will fail with an import error for apt_pkg and look like a package management problem on a single host.

Once you have pinned, the same command becomes the verification: the fact should be undefined on every host, because discovery no longer runs.

The virtualenv case

Some modules need a Python library on the target that is not packaged for the system interpreter, and that you would rather not install into the system interpreter — which on a modern distribution you may not be allowed to anyway, because PEP 668 marks the base environment as externally managed.

The answer is a virtualenv on the target, and a pin scoped to the play that needs it:

- name: Configure the database from the automation virtualenv
  hosts: databases
  gather_facts: true
  vars:
    ansible_python_interpreter: /opt/automation/venv/bin/python3
  tasks:
    - name: Ensure the application database exists
      community.postgresql.postgresql_db:
        name: app_production
        state: present

Three things about this deserve attention.

The pin is on the play, so it applies to that play’s tasks and nowhere else. The host remains a normal host for every other play.

Fact gathering runs under the virtualenv too, because setup is a module like any other. That is usually harmless — facts do not depend much on the interpreter — but the gathered ansible_python facts will describe the virtualenv, which surprises people reading a fact cache later.

The virtualenv has to exist before the play runs, and creating it is a different play, running under the system interpreter, because the virtualenv is not there yet. That ordering is not optional and it is the usual reason a first attempt fails.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A play needs a module whose Python library is installed in /opt/automation/venv on the database hosts. Where should ansible_python_interpreter be set?

  2. Q2. Why is /usr/bin/python3 usually a better pin than /usr/bin/python3.12 on a Debian or RHEL-family host?

  3. Q3. Which of these are true consequences of pinning ansible_python_interpreter? Select all that apply.

  4. Q4. Before pinning an interpreter across a group, you should first check which interpreter each host in that group currently discovers.

Passing score: 75%. Answers are checked in this browser.