dnspython review
dnspython 2.8.0 gives Python code both resolver-level DNS lookups and direct access to names, messages, record data, zones, transfers, DNSSEC, and dynamic updates. It handles nearly every record type and can send queries over UDP, TCP, TLS, HTTPS, or experimental QUIC through synchronous and asynchronous APIs. Version 2.8.0 raises the floor to Python 3.10, adds a B-tree-backed zone implementation, introduces helpers for persistent query sockets, expands Windows resolver discovery, and adds the draft DSYNC record.
dnspython 2.8.0 installed as 1 package in 0.2 seconds, occupied 2 MB, and produced 0 pip-audit findings in our sandbox; choose it when DNS records or protocol messages matter. Use socket.getaddrinfo for ordinary host resolution that must honor the machine's full name-service configuration.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 2 MB |
| Import | ✓ | import dns in 0.02s · pure Python · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does dnspython install cleanly?
Yes. In a fresh container with an empty cache, pip install dnspython finished in 0.2s, leaving 1 package and 2 MB on disk. pip-audit reported no known vulnerabilities.
What does dnspython need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import dns succeeded in 0.02s, and the package ships py.typed for type checkers.
dnspython or pycares: which should you use?
pycares: Use the direct c-ares bindings when asynchronous resolver throughput matters more than dnspython's zone and message APIs. dnspython 2.8.0 installed as 1 package in 0.2 seconds, occupied 2 MB, and produced 0 pip-audit findings in our sandbox; choose it when DNS records or protocol messages matter.
When should you not use dnspython?
You only need the operating system's hostname lookup. The README recommends socket.getaddrinfo because dnspython does not consult /etc/hosts.
Discussed on
Use it if
- You need parsed MX, TXT, SRV, CAA, PTR, SOA, DNSKEY, or another record type that socket.getaddrinfo does not expose.
- A diagnostic must query chosen authoritative or recursive nameservers with explicit timeout and transport settings.
- The program edits zone files, performs AXFR or IXFR, or sends RFC 2136 updates signed with TSIG.
- DNS over HTTPS, TLS, DNSSEC validation, or raw DNS packet work must stay inside Python.
- You only need the operating system's hostname lookup. The README recommends socket.getaddrinfo because dnspython does not consult /etc/hosts.
- The caller expects automatic caching. A new Resolver has no cache until you assign dns.resolver.Cache or another cache implementation.
- Large asyncio batches need c-ares throughput more than zone, DNSSEC, and record-model breadth; pycares or aiodns is the narrower native-backed option.
- Optional transports must work from the base install. DoH, DNSSEC, IDNA, Trio, Windows WMI, and QUIC each require an extra, and missing support appears when that path runs.
- The team wants one generic failure value. NXDOMAIN, NoAnswer, NoNameservers, timeout, and a DNS response code represent different conditions that correct DNS software must preserve.
Setup reality
We installed dnspython 2.8.0 in a fresh Python 3.12 Bookworm sandbox in 0.2 seconds. The result was 1 installed package occupying 2 MB, and pip-audit reported 0 known vulnerabilities. import dns completed in 0.02 seconds. The distribution is pure Python, requires Python 3.10 or newer, carries the ISC license, and ships a py.typed marker. Our package measurement reported 21 direct dependencies.
The PyPI name is dnspython, while code imports dns; a local dns.py file will shadow it. The basic resolver path needs no credential. Feature extras are separate: use dnspython[doh], [dnssec], [idna], [trio], [wmi], or [doq] as needed. Version 2.8.0 also added socket and SSL-context helpers for callers that maintain persistent query connections.
Resolver reads system DNS configuration by default but skips /etc/hosts. timeout limits an individual attempt and lifetime limits the entire resolution. No cache is attached automatically, so repeated calls repeat network work unless you set resolver.cache. Each answer retains typed record objects, TTL data, and canonical names rather than returning plain strings.
Async calls still create a coroutine per question; bound bulk work with a semaphore. Raw dns.query functions return messages whose response code needs checking, while resolver methods translate common outcomes into exceptions. Use udp_with_fallback when a truncated UDP answer must retry over TCP. Zone transfers and updates usually also need server permission, TCP reachability, and TSIG keys.
Patterns
Read A records resolve-address
import dns.resolver
answer = dns.resolver.resolve('example.com', 'A')
for record in answer:
print(record.address)
print(answer.rrset.ttl)dnspython 2.x documents resolve(). This path queries DNS and does not read /etc/hosts.
Keep DNS failures separate distinguish-failures
import dns.exception
import dns.resolver
try:
answer = dns.resolver.resolve(name, 'MX')
except dns.resolver.NXDOMAIN:
print('name absent')
except dns.resolver.NoAnswer:
print('name exists without MX')
except dns.resolver.NoNameservers:
print('servers failed')
except dns.exception.Timeout:
print('deadline expired')NXDOMAIN means the name does not exist; NoAnswer means it exists but has no requested rrset.
Use chosen resolvers and deadlines query-nameserver
resolver = dns.resolver.Resolver(configure=False)
resolver.nameservers = ['1.1.1.1', '8.8.8.8']
resolver.timeout = 2.0
resolver.lifetime = 5.0
answer = resolver.resolve('example.com', 'AAAA')timeout applies per attempt; lifetime is the budget across retries and nameservers.
Reassemble a TXT record join-txt-chunks
for record in dns.resolver.resolve('_dmarc.example.com', 'TXT'):
value = b''.join(record.strings).decode('utf-8')
print(value)TXT data may arrive as multiple byte strings because one wire-format character string is limited to 255 octets.
Bound async lookups resolve-asynchronously
import asyncio
import dns.asyncresolver
limit = asyncio.Semaphore(100)
async def lookup(name):
async with limit:
answer = await dns.asyncresolver.resolve(name, 'A')
return [r.address for r in answer]One coroutine is created for each lookup, so the semaphore prevents a large input list from flooding sockets and resolvers.
Send a DNS-over-HTTPS request query-over-https
import dns.message
import dns.query
query = dns.message.make_query('example.com', 'A')
response = dns.query.https(
query,
'https://cloudflare-dns.com/dns-query',
timeout=5
)
print(response.answer)Install `dnspython[doh]` first. The base installation does not supply the HTTP dependencies.
Retry a truncated UDP response fallback-to-tcp
import dns.message
import dns.query
query = dns.message.make_query('example.com', 'DNSKEY')
response, used_tcp = dns.query.udp_with_fallback(
query, '1.1.1.1', timeout=3
)
print(response.rcode(), used_tcp)The raw query layer leaves DNS response-code interpretation to the caller.
Replace a record with TSIG send-dynamic-update
import dns.query
import dns.tsig
import dns.tsigkeyring
import dns.update
keys = dns.tsigkeyring.from_text({'update-key.': secret})
update = dns.update.Update(
'example.com', keyring=keys,
keyalgorithm=dns.tsig.HMAC_SHA256
)
update.replace('www', 300, 'A', '203.0.113.10')
response = dns.query.tcp(update, '198.51.100.53', timeout=10)
print(response.rcode())`replace` overwrites the rrset. The server must permit the key and may report refusal through the DNS response code rather than a Python exception.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pycares | PyPI | Use the direct c-ares bindings when asynchronous resolver throughput matters more than dnspython's zone and message APIs. |
| aiodns | PyPI | Use its asyncio wrapper around pycares for many concurrent address and common record lookups. |
| dnslib | PyPI | Use it when the main task is parsing packets or implementing a small DNS server or proxy. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

