Skip to main content
RunBook Academy

AnsibleXLVI · Python and Interpreter DiscoveryRemote Python and interpreter discovery

What actually runs on a managed node

Intermediate⏱ ~20 minansible-coreansible-doc

What you'll learn

  • State exactly what a managed node must provide for a normal module to run
  • List what Ansible does not install or leave behind on a managed node
  • Explain why controller Python 3.12+ and managed-node Python are independent requirements
  • Predict which of a set of tasks would still work on a host with no Python
  • Recognise the failure signature of a missing or unusable target interpreter

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.

“Ansible is agentless” is true and is routinely over-read. It does not mean nothing runs on the managed node. It means nothing runs on the managed node between your runs.

During a run, quite a lot happens there. A module is a Python program. The controller assembles it, ships it over the transport, and the managed node executes it with a Python interpreter of its own. The module prints a JSON document to stdout; the controller reads that document and turns it into the ok or changed you see on the terminal.

That single sentence is the whole contract, and almost every question in this part is a consequence of it.

Part II established how the payload is built and shipped — AnsiballZ, the temporary directory, pipelining. This lesson is about what the far end has to provide for that to work at all, and what happens to an estate where some hosts cannot provide it.

The contract, stated precisely

For a normal module — apt, service, file, user, copy, and the several thousand others — the managed node must offer three things.

A transport Ansible can reach it on. Usually SSH, and usually with a POSIX shell available for the account you connect as. The connection plugin owns this; Part II covered the alternatives.

A POSIX shell. Ansible does not execute the module directly. It executes a shell command line that in turn invokes the interpreter. That is why the shell plugin (ansible.builtin.sh) exists as a separate plugin from the connection plugin, and why a host with a locked-down non-shell login is awkward even when SSH works.

A Python interpreter the module payload can run under. This is the requirement people forget, and it is the subject of this part.

That is the list. Read what is not on it:

  • No Ansible installation on the target. The managed node has no idea what Ansible is.
  • No daemon, no agent, no listening port that Ansible added.
  • No persistent state. The temporary directory holding the payload is removed when the module finishes.
  • No matching version. The target does not run the same Python as the controller, and does not need to.
Read-only / Safe

Running the first of those against ansible-core 2.21.3 returns, among the attributes:

Read-only / Safethe platform attribute of raw
$ ansible-doc ansible.builtin.raw | grep -A4 'platform:'
        platform:
      description: Target OS/families that can be operated against
      details: This action is one of the few that requires no Python on the remote as it
        passes the command directly into the connection string
      platforms: all

“One of the few” is doing real work in that sentence. The set is small, and lesson 5 of this part is about how small.

Two Pythons, two jobs

The course has said several times that controller Python and managed-node Python are separate requirements. Here is the reason.

ControllerManaged node
Runsansible-playbook, Jinja, the plugin systemone module payload at a time
Version required by core 2.213.12 – 3.143.9 – 3.14
Enforced bypip, at install timenothing, until a module fails
Owned bythe automation teamwhoever owns the host
Changing ita controller rebuilda host change window

The asymmetry in the last three rows is the operationally important part. The controller requirement fails loudly, early, on one machine you control. The managed-node requirement fails quietly, late, on a machine somebody else owns, in the middle of a run.

What the target interpreter has to be able to do

Not much, and that is deliberate. The payload is written to be importable and runnable by a plain interpreter with the standard library. It does not import ansible, because ansible is not there.

But “plain interpreter with the standard library” hides two real constraints.

The payload must parse. Module source uses the syntax of the Python versions upstream still supports. When the managed-node floor rises from 3.8 to 3.9, it rises because module code has started using something 3.8 cannot parse. The failure therefore arrives as a SyntaxError from inside the payload, not as a polite version check.

Some modules need bindings that are not in the standard library. This is the one that costs afternoons. ansible.builtin.apt needs the apt and apt_pkg Python modules, which arrive on Debian and Ubuntu as the distribution package python3-apt and are installed for one specific interpreter. ansible.builtin.dnf5 needs the libdnf5 bindings in the same way. SELinux modules need python3-libselinux.

A module that needs bindings is only as portable as the interpreter that has them. That is the mechanism behind the most common interpreter incident in this course, and lesson 3 is about pinning your way out of it.

Which tasks survive without Python

This is worth working through now, because the intuition most people have is wrong.

TaskNeeds Python on target?
ansible.builtin.packageyes
ansible.builtin.fileyes
ansible.builtin.commandyes
ansible.builtin.shellyes
ansible.builtin.setup (fact gathering)yes
ansible.builtin.rawno
ansible.builtin.scriptno

command and shell are the entries that surprise people. They run a shell command on the target, so they feel like they should be exempt. They are not: both are ordinary Python modules that happen to invoke a subprocess. The payload still has to be shipped and executed by an interpreter before the subprocess exists.

Only raw and script are genuinely interpreter-free, and both say so in their own documentation — script describes itself as not requiring Python on the remote system, “much like the ansible.builtin.raw module”. script transfers a local file and executes it through the remote shell; raw passes the command straight into the connection string.

The failure signature

A host with no usable interpreter does not produce a message that says “install Python”. It produces one of these.

No interpreter found. Discovery tried the candidate list, found nothing, fell back to /usr/bin/python3, and that is not there either:

[WARNING]: No python interpreters found for host 'edge-07.example.com'
(tried ['python3.14', 'python3.13', 'python3.12', 'python3.11',
'python3.10', 'python3.9', '/usr/bin/python3', 'python3']).

followed by the first module failing with a message about /usr/bin/python3 not existing.

An interpreter that is too old. The payload arrives and fails to parse, so you get a SyntaxError with a traceback pointing inside a temporary path under .ansible/tmp — a filename that does not exist by the time you go looking for it, because the payload directory is cleaned up.

An interpreter without the bindings. The module runs and fails with an import error naming a library, most famously:

Failed to import the required Python library (apt) on
web-a3.example.com's Python /usr/bin/python3.13.

Read that message carefully when you meet it: it tells you which interpreter it used. In an estate where discovery is unpinned, that path is the diagnosis.

Blast radius

Reading this lesson as an operator rather than a student, the question is: how many of my hosts could fail this way, and would I find out before or during a change window?

The answer is available without running anything against them, because the interpreter is a gathered fact and a fact cache already holds it if you use one. Where it is not cached, one fact-gathering run answers it for the whole estate:

Read-only / Safethe interpreter every host actually used
$ ansible all -m setup -a 'filter=ansible_python_version' --one-line
web-a1.example.com | SUCCESS => {"ansible_facts": {"ansible_python_version": "3.12.3", ...}, "changed": false}
web-a2.example.com | SUCCESS => {"ansible_facts": {"ansible_python_version": "3.12.3", ...}, "changed": false}
db01.example.com | SUCCESS => {"ansible_facts": {"ansible_python_version": "3.9.21", ...}, "changed": false}
edge-07.example.com | UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh", "unreachable": true}

Illustrative output

Three supported hosts and one that did not answer. The unreachable host is Part XLIV’s subject, not this one — but note that an unreachable host and a host with no Python look nothing alike, which is genuinely useful: you can tell them apart from the summary line without investigating.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A colleague says Ansible is agentless, so nothing runs on the managed node. What is the accurate correction?

  2. Q2. Which of these still work against a managed node that has no Python interpreter at all? Select all that apply.

  3. Q3. A managed node running Python 3.9 can be managed by a controller running ansible-core 2.21 on Python 3.13.

  4. Q4. A run fails on one host with: Failed to import the required Python library (apt) on that host Python /usr/bin/python3.13. What does the message tell you?

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