AnsibleIII · Installing and Designing the ControllerChoosing what to install
The support matrix, and which Python is which
What you'll learn
- Read the ansible-core support matrix and state both Python requirements separately
- Answer "can this controller manage that host" from the table rather than by trial
- Explain why a distribution package of Ansible usually lags the matrix
- Diagnose a single-host failure in a mixed-age fleet without blaming the controller
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
Ansible has two Python requirements, and almost every question about version compatibility becomes easy the moment you stop treating them as one.
The controller runs ansible-playbook. It needs a Python that the
installed ansible-core supports, and that requirement is strict — pip
will refuse the install outright if it is not met.
The managed node executes the module payload the controller ships it. It needs a Python in a much wider, much older range, because a fleet contains machines built over a decade.
These are different numbers. They move on different schedules. They fail in different ways.
The matrix
For ansible-core releases around the one this course targets:
ansible-core | Controller Python | Managed-node Python |
|---|---|---|
| 2.21 | 3.12 – 3.14 | 3.9 – 3.14 |
| 2.20 | 3.12 – 3.14 | 3.9 – 3.14 |
| 2.19 | 3.11 – 3.13 | 3.8 – 3.13 |
| 2.18 | 3.11 – 3.13 | 3.8 – 3.13 |
Read it as two independent questions:
- Will
ansible-core2.21 install and run on this controller? Look at column two. - Can a controller running 2.21 manage this host? Look at column three, against whatever Python the host has.
The controller requirement is not a recommendation. It is encoded in the package metadata, and pip enforces it:
$ python -c "import importlib.metadata as m; d = m.metadata('ansible-core'); print(d['Version'], d['Requires-Python'])"2.21.3 >=3.12The upper bound is not expressed as a metadata constraint — it is the range upstream tests and supports, published in the matrix and reflected in the package classifiers:
$ python -c "import importlib.metadata as m; [print(c) for c in m.metadata('ansible-core').get_all('Classifier') if 'Python :: 3.' in c]"Programming Language :: Python :: 3.12
Programming Language :: Python :: 3.13
Programming Language :: Python :: 3.14Why distribution packages lag
An estate that installs Ansible with apt install ansible gets whatever
its release froze. That is the whole design of a stable distribution and
it is not a defect — but it means the version you get is a property of
your operating system release date, not of what Ansible currently ships.
$ apt-cache policy ansible ansible-coreansible:
Installed: 13.1.0+dfsg-1ubuntu1
Candidate: 13.1.0+dfsg-1ubuntu1
Version table:
*** 13.1.0+dfsg-1ubuntu1 500
500 http://archive.example.com/ubuntu resolute/universe amd64 Packages
100 /var/lib/dpkg/status
ansible-core:
Installed: 2.20.1-1
Candidate: 2.20.1-1
Version table:
*** 2.20.1-1 500
500 http://archive.example.com/ubuntu resolute/universe amd64 Packages
100 /var/lib/dpkg/statusThat is a real distribution offering community package 13.1.0, which is
ansible-core 2.20.1 — one minor release behind the 2.21.x this course
targets. There is nothing wrong with 2.20. But if your repository
declares 2.21 and your controller is a distribution package, the
declaration is fiction, and every claim you have verified was verified
against something else.
The gap grows with the age of the distribution release. On a
long-term-support base two years into its life, the packaged
ansible-core can be three or four minor releases behind, with
deprecations already removed upstream that your playbooks still rely on
— or, more awkwardly, deprecation warnings you have never seen because
your version does not emit them yet.
Answering the real question from the table
The question that comes up in practice is not “which Python do I have”. It is “can this controller manage that host” — usually asked about the oldest machine in the estate, ten minutes before a change window.
Work it in two steps.
Step one: what does the controller run?
$ ansible --versionansible [core 2.21.3]
config file = /home/ops/estate/ansible.cfg
configured module search path = ['/home/ops/.ansible/plugins/modules']
ansible python module location = /home/ops/.venvs/estate/lib/python3.12/site-packages/ansible
executable location = /home/ops/.venvs/estate/bin/ansible
python version = 3.12.7
jinja version = 3.1.6Illustrative output
Core 2.21, controller Python 3.12. Row one of the matrix, and it is within 3.12 – 3.14. The controller is fine.
Step two: what does the managed node run? Ask the host, not the
matrix. If you can already reach it with Ansible, the fact is gathered
for you; if you cannot, ssh and python3 --version answers it.
The matrix then says: 3.9 through 3.14 is supported by core 2.21. A RHEL 9 host with the platform Python 3.9 is inside the range. A long-abandoned box with Python 3.6 is not, and no amount of controller upgrading will change that — upgrading the controller moves the managed-node floor up, not down.
The failure that gets misdiagnosed
A run against ninety hosts succeeds on eighty-nine and fails on one, with a Python traceback in the module output rather than a clean Ansible error message.
The instinct is to suspect the controller — it is the machine that changed, it is the machine you can log into, and the error mentions Python. That instinct is wrong here, and following it costs an hour.
The controller’s Python is a single fact shared by all ninety hosts. If
it were the problem, all ninety would have failed. A failure on one
host in an otherwise healthy run is a property of that host, and on a
mixed-age fleet the property is very often its interpreter: an old
platform Python below the supported floor, or a host where python3
resolves to something surprising.
The diagnosis is a single fact gathered from the failing host:
$ ansible db07.example.com -m setup -a 'filter=ansible_python' | head -20db07.example.com | SUCCESS => {
"ansible_facts": {
"ansible_python": {
"executable": "/usr/bin/python3.8",
"has_sslcontext": true,
"type": "cpython",
"version": {
"major": 3,
"minor": 8,
"micro": 18,
"releaselevel": "final",
"serial": 0
},
"version_info": [3, 8, 18, "final", 0]
}
},
"changed": false
}Illustrative output
Python 3.8, against a core 2.21 floor of 3.9. The host is below the matrix. The controller is innocent, the playbook is innocent, and the fix is on the managed node — which is a different team’s ticket and a different change window, so knowing this in ten minutes rather than two hours matters.
Knowledge check
Knowledge check · 4 questions
Q1. A controller runs ansible-core 2.21 on Python 3.13. A managed node runs Python 3.9. What does the support matrix say?
Q2. Upgrading a controller from ansible-core 2.19 to 2.21 can make some previously managed hosts unmanageable.
Q3. A run against 90 hosts succeeds on 89 and fails on one with a Python traceback in the module output. What is the most likely cause?
Q4. Which statements about a distribution-packaged Ansible are accurate? Select all that apply.
Passing score: 75%. Answers are checked in this browser.