APiHole
A small Python client for the Pi-hole v6 REST API.
Still on Pi-hole v5? Version 1.0 and later only work with Pi-hole v6. Pin the old release with
pip install 'APiHole==0.0.3'. Its source stays on thev5-legacybranch. It is frozen, since Pi-hole v5 is end-of-life.
Install
pip install APiHole
Requires Python 3.9+.
Authentication: use an app password
Pi-hole v6 authenticates with sessions. APiHole logs in with a password, sends the session id with every request, and logs out when you are done.
Don't give scripts your admin password. Create an app password instead:
- In the Pi-hole web interface, open Settings → Web interface / API.
- Click Configure app password, copy the generated password, and click Enable new app password.
An app password also skips two-factor authentication. If your Pi-hole has no
password at all, pass password=None and APiHole does not log in.
Pi-hole allows only a limited number of API sessions at once. Always use
with(or callclose()) so the session is released. Scripts that leak sessions eventually getRateLimitError(HTTP 429) until the old sessions expire.
Usage
from APiHole import PiHole
with PiHole("pi.hole", "your-app-password") as ph:
print(ph.version()["ftl"]["local"]["version"])
print(ph.summary()["queries"]["blocked"])
ph.disable(30) # pause blocking for 30 seconds
ph.enable()
ph.deny("ads.example.com", comment="added from script")
ph.remove_deny("ads.example.com")
Connection options:
PiHole(
host="192.168.1.2:8080", # hostname or IP, optional port
password="...", # None if the Pi-hole has no password
https=True, # default False
verify=False, # TLS verification, or a path to a CA bundle
timeout=10, # seconds per request
totp=None, # 2FA code, only if not using an app password
)
Methods
| Method | Returns |
|---|---|
version() |
dict of core/web/FTL/docker versions |
summary() |
dict with queries, clients, gravity counters |
gravity_last_update() |
datetime (UTC) or None |
blocking_status() |
{"blocking": "enabled" | "disabled" | …, "timer": …} |
enable() / disable(seconds=None) |
new blocking status; None or 0 means indefinitely |
top_domains(count=10, blocked=False) |
list of {"domain", "count"} |
top_clients(count=10, blocked=False) |
list of {"ip", "name", "count"} |
recent_blocked(count=1) |
list of domain names |
upstreams() |
list of upstream servers with counts |
query_types() |
dict of record type → count |
cache_info() |
DNS cache metrics |
clients() |
network devices |
history() |
10-minute buckets of total/blocked/cached/forwarded |
history_clients() |
per-client 10-minute buckets |
dns_port() |
int |
allow(domain, comment=None) / remove_allow(domain) |
allow list, exact |
allow_regex(pattern, comment=None) / remove_allow_regex(pattern) |
allow list, regex |
deny(domain, comment=None) / remove_deny(domain) |
deny list, exact |
deny_regex(pattern, comment=None) / remove_deny_regex(pattern) |
deny list, regex |
update_gravity(timeout=600) |
gravity log output (pihole -g) |
restart_dns() |
None |
For anything else, request(method, path, **kwargs) calls any endpoint under
/api with the same session handling and returns the requests.Response.
Your Pi-hole documents every endpoint at http://pi.hole/api/docs.
Errors
Every error is a subclass of PiHoleError:
| Exception | When |
|---|---|
PiHoleConnectionError |
the host could not be reached or timed out |
AuthenticationError |
401: wrong password or missing 2FA code |
NotFoundError |
404: e.g. removing a domain that is not on the list |
RateLimitError |
429: too many requests or no free API sessions |
APIError |
any other HTTP error; has status_code, key, message, hint |
If a session expires in the middle of a script, APiHole logs in again once and retries the request.
APiHole logs to the APiHole logger and never configures logging itself.
Migrating from 0.0.3
1.0 is a clean break. Create a client once, not a static call per request. Whitelist/blacklist are now called allow/deny, as in Pi-hole itself.
| 0.0.3 | 1.0 |
|---|---|
PiHole.GetVer(IP) |
ph.version() |
PiHole.GetSummary(IP, API, Raw_data) |
ph.summary() (always raw numbers) |
PiHole.GetStatus(IP, API) |
ph.blocking_status() |
PiHole.GetGravity(IP, API) |
ph.gravity_last_update() |
PiHole.Enable(IP, API) |
ph.enable() |
PiHole.Disable(IP, API, Time) |
ph.disable(seconds) |
PiHole.GetTopItems(IP, API, n) |
ph.top_domains(n) and ph.top_domains(n, blocked=True) |
PiHole.GetTopClients(IP, API, n) |
ph.top_clients(n) |
PiHole.GetTopClientsBlocked(IP, API, n) |
ph.top_clients(n, blocked=True) |
PiHole.GetRecentBlocked(IP, API) |
ph.recent_blocked() |
PiHole.GetDestination(IP, API) |
ph.upstreams() |
PiHole.GetQueryTypes(IP, API) |
ph.query_types() |
PiHole.GetCacheInfo(IP, API) |
ph.cache_info() |
PiHole.GetClientNames(IP, API) |
ph.clients() |
PiHole.GetOverTimeData10mins(IP, API) |
ph.history() |
PiHole.GetOverTimeDataClients(IP, API) |
ph.history_clients() |
PiHole.GetDnsPort(IP, API) |
ph.dns_port() |
PiHole.AddWhite(IP, API, d) |
ph.allow(d) |
PiHole.AddBlock(IP, API, d) |
ph.deny(d) |
PiHole.RemoveBlock(IP, API, d) |
ph.remove_deny(d) |
PiHole.AddRegexBlock(IP, API, r) |
ph.deny_regex(r) |
PiHole.RemoveRegexBlock(IP, API, r) |
ph.remove_deny_regex(r) |
Other changes:
- Failures raise exceptions instead of returning
NoneorTrue. - The API token from
setupVars.confno longer exists. Use an app password. - Response shapes follow the v6 API, so dict keys differ from v5.
Development
python -m venv .venv
.venv/bin/pip install -e ".[test]"
.venv/bin/pytest
example.py is a smoke test against a real Pi-hole:
PIHOLE_HOST=pi.hole PIHOLE_PASSWORD=... python example.py.
Feedback
Open an issue on GitHub or email shmulik.debby@gmail.com.
Release files for APiHole 1.0.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 | |
|---|---|---|---|
| apihole-1.0.0.tar.gz | 12.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| apihole-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 21.8 kB
Release files / apihole-1.0.0.tar.gz
| Download URL | apihole-1.0.0.tar.gz |
|---|---|
| Size | 12.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3008f291715cae855e75253c3227c6ae8ea6b6ae884e2eab1a048f15e3c5c911
|
|
BLAKE2b-256 checksum How to use checksums |
c7c18e2e15e2c540716c0ea05fe186e724db3e43b891ea30cc4e476a94b51990
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 26, 2026.
Transparency logRelease files / apihole-1.0.0-py3-none-any.whl
| Download URL | apihole-1.0.0-py3-none-any.whl |
|---|---|
| Size | 9.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a6d101bede913cb59d31b5ca5779aaef2ee80b5936e4dae6181d6738e422e54b
|
|
BLAKE2b-256 checksum How to use checksums |
68ad840cc0f568a06e5256971b132afac93833f3b4bfb5723d7fdc6e595a8e49
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 26, 2026.
Transparency log