ansible-host
A thin Python wrapper over Ansible's executor. Lets you run Ansible modules from Python with structured results, batch execution, and a host-object-first API — without going through pytest-ansible or ansible-runner.
Status: beta (0.1.0). Built from a working internal implementation; API is stable in shape but may shift in details before 1.0.
Why this exists
Ansible has a great executor and a huge module ecosystem, but the existing Python access paths are awkward for in-test or in-tool use:
pytest-ansible |
ansible-runner |
ansible-host |
|
|---|---|---|---|
| Use case | Pytest fixtures | AWX-style managed jobs | In-process programmatic |
| API surface | ~20% of Ansible's runtime | Subprocess + event stream | Typed Python objects |
| Forking, custom callbacks | Limited | Yes (in subprocess) | Yes (native) |
| Per-call overhead | Low | High (subprocess + JSON parsing) | Low (in-process) |
| Returns | Strings | Event stream | Structured Python dicts |
ansible-host sits where the other two don't: in-process, low-overhead, structured-result execution that you can drop into any Python codebase that wants Ansible underneath.
Install
pip install ansible-host
Requires Python 3.10+ and ansible-core>=2.16,<2.22. Like Ansible itself, the library runs on POSIX systems (Linux, macOS, WSL) — it is not supported on native Windows.
Development
The recommended dev workflow uses uv — it manages the virtualenv directly, sidestepping the python3-venv split on Ubuntu and installing dependencies an order of magnitude faster than pip.
# One-time: install uv (https://docs.astral.sh/uv/getting-started/installation/)
curl -LsSf https://astral.sh/uv/install.sh | sh
# In the repo
uv venv # creates .venv with the default Python
uv pip install -e ".[dev]" # editable install + dev extras
uv run pytest # run the test suite
uv run ruff check src tests # lint
To target a specific Python or ansible-core version (matches the CI matrix):
uv venv --python 3.12
uv pip install -e ".[dev]"
uv pip install "ansible-core==2.19.*"
uv run pytest
pip and a manually-managed venv still work — uv is just the convenience.
Testing
The suite has two tiers:
- Tier-1 (default) — runs modules in-process over Ansible's
localconnection. No SSH or external host required; this is whatuv run pytestand CI's matrix run. - Tier-2 (
sshmarker) — exercises Ansible's real SSH transport (connection options, remote module exec,become, multi-host fanout) against a throwaway containerized sshd intests/ssh/. These tests auto-skip unless an SSH target is configured, so the default run stays green without any SSH setup.
Run the SSH tier locally (requires Docker):
# Start a throwaway SSH target with an ephemeral key authorized on it
ssh-keygen -t ed25519 -N '' -f /tmp/ah_ssh_key
export AH_SSH_PUBLIC_KEY="$(cat /tmp/ah_ssh_key.pub)"
docker compose -f tests/ssh/docker-compose.yml up -d --build
# Point the ssh-marked tests at it
AH_SSH_HOST=127.0.0.1 AH_SSH_PORT=2222 AH_SSH_USER=ansible AH_SSH_KEY=/tmp/ah_ssh_key \
uv run pytest -m ssh
# Tear down
docker compose -f tests/ssh/docker-compose.yml down -v
CI runs Tier-1 across the full Python × ansible-core matrix, plus a dedicated test-ssh job that builds the container and runs the Tier-2 tests.
Quickstart
30-second try — no SSH, no inventory
AnsibleLocalhost runs modules in-process on the current machine via Ansible's local connection plugin. No inventory file, no SSH, no setup.
from ansible_host import AnsibleLocalhost
host = AnsibleLocalhost()
result = host.ping()
assert result["ping"] == "pong"
result = host.command("uname -a")
print(result["stdout"])
Running against a real host
Drop an inventory file alongside your script:
# inventory.ini
[switches]
sw-01 ansible_host=10.0.0.1 ansible_user=admin
from ansible_host import AnsibleHost
host = AnsibleHost(inventory="inventory.ini", pattern="sw-01")
result = host.shell("show version")
print(result["stdout"])
Multi-host fanout
from ansible_host import AnsibleHosts
hosts = AnsibleHosts(inventory="inventory.ini", pattern="switches")
results = hosts.ping() # parallelism via forks=
for hostname, r in results.items():
print(hostname, r["ping"])
# Container API: index, iterate, len
print(len(hosts), hosts.hostnames)
first = hosts[0] # -> AnsibleHost
by_name = hosts["sw-01"] # -> AnsibleHost
for h in hosts:
print(h.hostname)
Dynamic dispatch and task directives
Any Ansible module is callable as a method via __getattr__ (host.<module_name>(...)):
host.copy(src="/etc/hosts", dest="/tmp/hosts.bak")
host.command("rm /tmp/maybe-missing", task_directives={"ignore_errors": True})
host.shell("echo $TOKEN", task_directives={"no_log": True})
Common task_directives: ignore_errors, no_log, when, failed_when, changed_when, become.
Batch mode — queue tasks, run them in a single play
with host:
host.shell("uptime")
host.shell("df -h")
host.shell("free -m")
results = host.results
Building a queue across functions
with host: is lexically scoped. If you need to assemble a batch across multiple functions, use the explicit form — same machinery, no scope limit:
host.load_module("ansible.builtin.command", args=["uptime"])
host.load_module("ansible.builtin.command", args=["df -h"])
results = host.run_loaded_modules()
Result shapes
| single task | batch (with block) |
|
|---|---|---|
| single host | dict |
list[dict] |
| multi host | {hostname: dict} |
{hostname: list[dict]} |
Reading host variables
Inspect inventory variables resolved by Ansible — host vars, group vars, and runtime extra vars — without running a module:
host = AnsibleHost(inventory="inventory.ini", pattern="sw-01")
# Variables defined directly on the host (raw — Jinja2 templates not rendered):
host.get_host_var("ansible_user") # -> "admin"
host.host_vars # -> dict of host-scoped vars
# Fully-resolved view a host can see (host + group + extra vars), with
# Jinja2 templates rendered:
host.get_visible_var("some_group_var") # group var visible to the host
host.get_visible_var("missing", "default") # default when absent
host.visible_vars # -> dict of all resolved vars
# Runtime overrides (highest precedence):
host.update_extra_vars({"feature_flag": True})
host.extra_vars # -> current extra vars
AnsibleHosts exposes the host-keyed form: hosts.get_host_var("sw-01", "ansible_user")
and hosts.get_visible_var("sw-01", "some_var").
Failures
A failing module raises AnsibleModuleFailed:
from ansible_host import AnsibleModuleFailed
try:
host.command("false")
except AnsibleModuleFailed as e:
print("module failed:", e)
# Or suppress and inspect the result:
result = host.command("false", task_directives={"ignore_errors": True})
assert result["failed"] is True
More examples
See the test suite for runnable examples:
tests/test_local_integration.py— ping, command, shell, batch mode, dynamic dispatch,no_log,forks, multi-host fanout, per-host failure aggregation, and the container protocol.tests/test_host_vars.py— reading host/group/extra variables and precedence.tests/test_execution_edge_cases.py— error contracts, failure aggregation, unreachable hosts, andgather_facts.
Compatibility
This library uses Ansible's internal Python API (TaskQueueManager, InventoryManager, VariableManager, Play, DataLoader). Those APIs are not officially stable across ansible-core releases — expect occasional updates when ansible-core introduces breaking internal changes. The current support range is declared in pyproject.toml and in the matrix CI.
Concurrency
ansible-host is designed for sequential use at the instance level. For parallelism within a single call, use Ansible's native forking via the forks= option. Running multiple AnsibleHost / AnsibleHosts instances concurrently in different threads will race on ansible.context's process-global state.
Reference implementation
A real-world use of this pattern lives in sonic-mgmt's tbng branch — ansible-host is the cleaned-up, packaged version of that code.
License
Apache License 2.0. See LICENSE.
Release files for ansible-host 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ansible_host-0.1.0.tar.gz | 33.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ansible_host-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.5 kB
Release files / ansible_host-0.1.0.tar.gz
| Download URL | ansible_host-0.1.0.tar.gz |
|---|---|
| Size | 33.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1b032444ff07587886cb9679093fff1bb848144414acd6ac53b3994970b83bcf
|
|
BLAKE2b-256 checksum How to use checksums |
1b29eb7076b1ae24eef2d19a676758dc132d9b1ee2ec410dfe85d67944d4c6e5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 12, 2026.
Transparency logRelease files / ansible_host-0.1.0-py3-none-any.whl
| Download URL | ansible_host-0.1.0-py3-none-any.whl |
|---|---|
| Size | 23.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ead887c5668aaf4bd4afaa4ff0b573921713b4a124da8f99b50ac94e20a6b08d
|
|
BLAKE2b-256 checksum How to use checksums |
63866f9214d8221bca80e1fb075acc5e69ca33d5e342f15be67b11f4433e3c9d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jun 12, 2026.
Transparency log