AnsibleXLVI · Python and Interpreter DiscoveryRemote Python and interpreter discovery
Pinning the interpreter as a production decision
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
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.
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.
$ ansible webservers -m debug -a 'var=ansible_facts.discovered_interpreter_python' --one-lineweb-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
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?
Q2. Why is /usr/bin/python3 usually a better pin than /usr/bin/python3.12 on a Debian or RHEL-family host?
Q3. Which of these are true consequences of pinning ansible_python_interpreter? Select all that apply.
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.