mrkeyoor.com_
Sun 20 Sept 04:54 UTC
PyPIUtilsupdated 20 Sept 2026

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.

Verdict

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

Lab card: what happened when we installed dnspythonScreenshot of dnspython documentation
Install✓ · 0.2s1 package on disk · 2 MB
Importimport dns in 0.02s · pure Python · py.typed · requires Python >=3.10
Known vulns0(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.

API stability4/5The 2.x API keeps resolver calls, raw queries, names, messages, zones, updates, and record data in distinct modules, and the current documentation still uses `resolve()` introduced at the last major transition. Version 2.8 adds B-tree zones and connection helpers without displacing existing resolver code. Protocol-shaped objects reduce arbitrary churn, although draft records such as DSYNC can still change.
Docs4/5The stable Read the Docs site has API pages, release notes, resolver examples, async counterparts, and dedicated material for DNSSEC, zones, transfers, updates, and transports. The README clearly states that /etc/hosts is ignored and lists every optional extra. Readers still need DNS knowledge to interpret rrsets, authority sections, response codes, absolute names, and trust chains correctly.
Maintenance5/5GitHub reported a push on August 25, 2026, only 4 open issues and pull requests, 2,670 stars, and an unarchived repository. PyPI serves 2.8.0 while the main-branch README identifies ongoing 2.9.0 development. The 2.8 release changed the Python floor, added a zone backend and socket helpers, and extended Windows configuration rather than limiting work to packaging.
Ecosystem5/5PyPI Stats counted 69,310,421 downloads in the latest week. One package covers common resolution, typed record objects, wire messages, zones, transfer protocols, authenticated updates, DNSSEC, and encrypted transports. Separate extras connect it to httpx, cryptography, Trio, IDNA, Windows WMI, and QUIC only when those features are selected.

Discussed on

  1. hnTracing DNS resolution with dnspython, beanstalk and graphviz6 points

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.
Skip it if

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

PackageRegistryPick it when
pycaresPyPIUse the direct c-ares bindings when asynchronous resolver throughput matters more than dnspython's zone and message APIs.
aiodnsPyPIUse its asyncio wrapper around pycares for many concurrent address and common record lookups.
dnslibPyPIUse 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.