Running without Python on the host
A managed node usually needs Python. The raw module, gather_facts: no, bootstrapping Python, and the script module.
Why this matters
Ansible reaches a managed node over SSH and doesn't install anything there
ahead of time. But almost every module on that side runs through a Python
interpreter. On a fresh minimal image, in an Alpine or busybox container,
Python might not be there at all, and the playbook fails on the very first
task. This lesson covers why that happens and how to work with such a
host: the raw and script modules, disabling fact gathering, and
installing Python through Ansible itself.
Why Ansible needs Python on the host
Most Ansible modules are written in Python. When you run a task, Ansible packs the module together with its arguments into a single file, copies it to the managed node, and runs it there. The chain looks like this:
- The control node builds a Python module with its arguments.
- It copies it to the host over SSH (SFTP or SCP) into a temporary folder
~/.ansible/tmp/of the user it connects as. - It runs it with whatever interpreter it finds on the host, usually
/usr/bin/python3. Ansible looks for the path itself (interpreter discovery), and you can set it with theansible_python_interpretervariable. - It gets a JSON response back and removes the temporary files.
If the host has no Python, step 3 fails right away. That happens with fresh minimal images, containers without Python, and network hardware.
Here's what that looks like on Alpine with no Python. The playbook fails before the first task, during fact gathering:
TASK [Gathering Facts] *********************************************************
[WARNING]: No python interpreters found for host 'web1' (tried ['python3.13', ...,
'/usr/bin/python3', 'python3']).
[ERROR]: Task failed: Action failed: The following modules failed to execute: ansible.legacy.setup.
...
| The module interpreter '/usr/bin/python3' was not found. Consider overriding the configured interpreter path for this host. See stdout/stderr for the returned output.
...
fatal: [web1]: FAILED! => {... "module_stdout": "/bin/sh: /usr/bin/python3: not found\r\n", ... "rc": 127}, "msg": "The following modules failed to execute: ansible.legacy.setup."}
PLAY RECAP *********************************************************************
web1 : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
The key part of the message is module interpreter ... was not found and
rc: 127: the shell on the host couldn't find an interpreter. SSH itself
works fine, and the key was accepted.
The raw module
raw runs a command directly over SSH, without any Python modules on the
other side. Ansible just sends a string to the host's shell and collects
the output.
ansible all -m raw -a "uptime"
ansible all -m raw -a "cat /etc/os-release"
When you need raw:
- A fresh host with no Python (an image that was just brought up).
- Containers based on busybox or Alpine.
- Bootstrapping: install Python so you can switch to regular modules.
- A device where SSH gives you a shell, but there's no Python and never will be.
For network hardware (Cisco IOS, Junos) the standard path is different:
connection: network_cli and modules from the vendor's collections. They
run on the control node and talk to the device over SSH. raw is still a
fallback there.
raw understands regular inventory variables and connection parameters
(ansible_host, ansible_user, ansible_port, become).
gather_facts: false
Before running tasks, Ansible gathers facts about the host: it runs the
setup module, which finds out the OS, IP addresses, disks, and so on.
That module needs Python too.
On a host with no Python, fact gathering fails before the first task (see the output above). The fix: disable it at the play level.
- name: bootstrap host without Python
hosts: newservers
gather_facts: false # Python isn't installed yet
tasks:
- name: check connectivity
ansible.builtin.raw: uptime
no works the same as false, and both show up in the docs. In this
course we write false. Once Python is installed, you can gather facts
explicitly with a task using the setup module.
Bootstrapping Python via raw
A classic trick: install Python first with raw, then work with regular
modules from there on.
- name: bootstrap Python3 on Debian/Ubuntu
hosts: newservers
gather_facts: false
become: true
tasks:
- name: install python3
ansible.builtin.raw: test -e /usr/bin/python3 || (apt-get update && apt-get install -y python3)
changed_when: false
- name: gather facts
ansible.builtin.setup:
- name: install nginx
ansible.builtin.apt:
name: nginx
state: present
test -e /usr/bin/python3 || keeps a rerun from hitting the repository
again. changed_when: false removes the false changed, more on that
below.
After setup, the ansible_facts.* variables become available, and
regular modules work as usual. On RHEL, Rocky, and AlmaLinux the package is
installed with dnf install -y python3, on Alpine with apk add python3.
The script module
script also works with no Python on the host. It copies a local
script from the control node to the host and runs it there through a
shell. The interpreter is set by the shebang on the script's first line,
or by the executable parameter.
- name: run init script
ansible.builtin.script: files/bootstrap.sh
args:
creates: /etc/myapp/.bootstrapped # skip if the file already exists
creates checks a path on the host: if the file is there, the task is
skipped with status skipped. The opposite parameter, removes, runs the
script only if the file is there.
How script differs from raw:
rawruns the argument string directly.scriptcopies a file from the control node and runs it.- Neither needs Python. Neither is idempotent on its own.
Don't confuse these with command and shell: those are regular
modules, and they need Python on the host.
The honest take on raw and script
raw and script are for edge cases. They have no idea whether anything
on the host actually changed. On a zero return code, raw always reports
changed, even if the command changed nothing. On a non-zero code, the
task fails with failed and a non-zero return code message. So for
check-style commands you set changed_when: false by hand, and for
script you set creates or removes.
That's exactly why they're used sparingly: to install Python, initialize a host, do something there's no regular module for. After that, regular modules take over, since they check the current state on their own.
Common mistakes
- You added a
rawtask that installs Python, but didn't disablegather_facts. The playbook fails during fact gathering and never reachesraw. - You disabled
gather_facts, installed Python, and then useansible_factswithout callingsetup. The variables are empty or undefined. - You write
command: apt-get install -y python3instead ofraw.commanditself needs Python and fails with the same error. - You copy an install command from an example for a different
distribution:
apt-getisn't found on Alpine or RHEL (rc: 127). - You run
scriptwithoutcreates: every playbook run executes the script again and showschanged.
What to remember
Ansible modules run on the host through a Python interpreter. No Python, no regular modules, no fact gathering.
raw runs a command over SSH without Python. gather_facts: false turns
off fact gathering, which also needs Python.
Bootstrapping via raw: first install python3, then run setup, then
work with regular modules.
script copies a local script and runs it with no Python on the host.
raw and script aren't idempotent, so they're used sparingly, not as a
replacement for regular modules.
In the next lesson we'll cover idempotency and why a playbook made of plain shell commands works poorly.
What you'll need for the tasks
- Read the fact-gathering error and recognize that the host has no Python.
- Disable
gather_factsat the play level. - Install Python with a
rawtask using the right distribution's package manager command, and gather facts with thesetupmodule. - Run a local script with the
scriptmodule and stop it from running again withcreates. - Run
ansible-playbookinside the container:docker exec <container> ansible-playbook <file>.
Interview-style questions
Interview questions on this topic: 2
Included with practice access, $49 one-time. Get access
Answer out loud or in writing, then reveal the answer and honestly mark whether you knew it.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson ansible-basics-04-without-python
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress