mrkeyoor.com_
Thu 06 Aug 00:57 UTC
PyPIUtilsupdated 05 Aug 2026

dnspython

dnspython is a DNS toolkit that speaks the protocol directly instead of asking your operating system to resolve things for you. The high-level side is dns.resolver, where you ask for a name and record type and get back parsed answers with typed fields: MX records have preference and exchange, SRV records have port and target, SOA records have serial. The low-level side lets you build, sign, send, and parse individual DNS messages, load and edit zone files, run AXFR and IXFR transfers, and push dynamic updates with TSIG authentication. It supports UDP, TCP, DNS over TLS, DNS over HTTPS, and experimental DNS over QUIC, plus asyncio and trio variants of every query function. The import name is dns, not dnspython.

Verdict

The complete DNS implementation for Python, and the only sensible choice once you need record types, specific nameservers, zone files, or dynamic updates. Use socket.getaddrinfo instead when all you want is a hostname resolved the same way the rest of your machine resolves it.

API stability4/5The 2.x API has been steady since 2020, with resolve() replacing the old query() and both still present. Minor releases add transports rather than move things, though 2.0 was a hard break from 1.x and old snippets on the internet still use the 1.x style.
Docs4/5dnspython.readthedocs.io has a full API reference plus a What's New page per release and an examples directory in the repo. It documents the protocol faithfully, which means it assumes you already understand DNS terminology.
Maintenance5/5Pushed July 2026, and only 3 open issues (8 counting PRs) against a 20-year-old project, which is an unusually clean tracker. Bob Halley has maintained it since the Nominum days and 2.9.0 is already in development.
Ecosystem5/5Roughly 76 million weekly downloads, pulled in by eventlet, email validators, and most Python networking tooling; it is the assumed answer for DNS in Python, so Stack Overflow answers and blog posts target it.

Use it if

  • You need record types the standard library cannot give you: MX, TXT, SRV, CAA, DNSKEY, PTR, or SOA with their fields already parsed into attributes rather than raw bytes
  • You need to query a specific nameserver rather than whatever the host is configured to use, which is how you check propagation, compare authoritative servers, or test a resolver you are running
  • You are automating DNS records: dns.update with TSIG keys does RFC 2136 dynamic updates, and dns.zone reads, edits, and writes zone files
  • You want DNS over HTTPS or DNS over TLS from Python without shelling out to a resolver binary
  • You are writing mail or security tooling that needs SPF, DKIM, DMARC, or MTA-STS lookups, which are all TXT record parsing on specific subdomains
Skip it if

Setup reality

pip install dnspython gives you a pure-Python package with zero required dependencies on Python 3.10 or newer, and the import name is dns, which trips people up on first use and collides with any local dns.py file in your project. Every interesting feature is an extra: dnspython[doh] for DNS over HTTPS (httpx, h2, httpcore), dnspython[dnssec] for validation (cryptography 45+), dnspython[trio], dnspython[idna] for internationalized names, dnspython[doq] for the experimental QUIC transport, and dnspython[wmi] on Windows if registry scanning picks the wrong interface. You can combine them, for example dnspython[doh,dnssec]. Defaults matter: the resolver reads /etc/resolv.conf at construction, has a 2 second per-query timeout with a 5 second total lifetime, and does no caching at all unless you assign resolver.cache yourself.

Patterns

Look up A and AAAA recordsresolve-a-record

import dns.resolver

answers = dns.resolver.resolve("example.com", "A")
for rdata in answers:
    print(rdata.address)
print("ttl:", answers.rrset.ttl)

resolve() replaced query() in 2.0; query() still works but is deprecated. This does not read /etc/hosts, so a name you can ping may still raise NXDOMAIN here.

Handle the four ways a lookup failshandle-lookup-errors

import dns.resolver, dns.exception

try:
    answers = dns.resolver.resolve(name, "MX")
except dns.resolver.NXDOMAIN:
    return "name does not exist"
except dns.resolver.NoAnswer:
    return "name exists, no MX records"
except dns.resolver.NoNameservers:
    return "every nameserver refused or failed"
except dns.exception.Timeout:
    return "timed out"

NXDOMAIN and NoAnswer mean very different things and conflating them is the most common dnspython bug. Pass raise_on_no_answer=False if you would rather get an empty answer object back.

Read structured record typesmx-and-srv-records

import dns.resolver

mx = dns.resolver.resolve("example.com", "MX")
for r in sorted(mx, key=lambda r: r.preference):
    print(r.preference, r.exchange.to_text())

srv = dns.resolver.resolve("_sip._tcp.example.com", "SRV")
for r in srv:
    print(r.priority, r.weight, r.port, r.target)

exchange and target are dns.name.Name objects with a trailing dot, not strings. Call .to_text() or str() before putting them in a URL or a config file.

Read TXT records such as SPF or DMARCtxt-records

import dns.resolver

for rdata in dns.resolver.resolve("_dmarc.example.com", "TXT"):
    value = b"".join(rdata.strings).decode()
    print(value)

rdata.strings is a tuple of byte chunks because TXT records are split every 255 characters on the wire. Joining them is required for long DKIM keys, and forgetting to is why half-parsed DKIM records show up in production.

Query a chosen nameserver with your own timeoutsquery-specific-nameserver

import dns.resolver

res = dns.resolver.Resolver(configure=False)
res.nameservers = ["1.1.1.1", "8.8.8.8"]
res.timeout = 2.0     # per nameserver attempt
res.lifetime = 5.0    # total budget for the question
answers = res.resolve("example.com", "A")

# one-shot equivalent
answers = dns.resolver.resolve_at("1.1.1.1", "example.com", "A")

configure=False skips reading /etc/resolv.conf, which you want when checking a specific server. timeout and lifetime are separate: a short timeout with a long lifetime retries, the reverse gives up early.

Reverse an IP address to a PTR namereverse-lookup

import dns.resolver, dns.reversename

rev = dns.reversename.from_address("8.8.8.8")
print(rev)  # 8.8.8.8.in-addr.arpa.
name = dns.resolver.resolve(rev, "PTR")[0].target.to_text()
print(name)

from_address handles IPv6 nibble format too. A PTR record is not proof of ownership, so do not use reverse lookups for authentication.

Resolve concurrently with asyncioasync-resolution

import asyncio
import dns.asyncresolver

async def lookup(name):
    try:
        ans = await dns.asyncresolver.resolve(name, "A")
        return name, [r.address for r in ans]
    except Exception as exc:
        return name, exc

results = asyncio.run(asyncio.gather(*(lookup(n) for n in names)))

dns.asyncresolver mirrors dns.resolver, and the backend is chosen from the running event loop, so the same code works under trio if you installed dnspython[trio]. Concurrency is still one Python coroutine per query, so cap it with a semaphore at a few hundred.

Query over DNS over HTTPSdns-over-https

import dns.message, dns.query

q = dns.message.make_query("example.com", "A")
resp = dns.query.https(q, "https://cloudflare-dns.com/dns-query", timeout=5)
for rrset in resp.answer:
    print(rrset.to_text())

Needs pip install dnspython[doh]; without it you get an ImportError for httpx at call time, not at install. dns.query.tls() is the equivalent for DNS over TLS on port 853.

Send a hand-built message over UDP with TCP fallbackraw-message-query

import dns.message, dns.query, dns.flags

q = dns.message.make_query("example.com", "NS")
q.flags &= ~dns.flags.RD          # do not recurse, ask authoritatively
resp, used_tcp = dns.query.udp_with_fallback(q, "198.51.100.53", timeout=3)
print(resp.rcode(), used_tcp)

udp_with_fallback retries over TCP when the answer comes back truncated, which is what any real client must do. The raw layer never raises NXDOMAIN; you read resp.rcode() yourself.

Pull a zone with AXFR and iterate the recordszone-transfer

import dns.zone, dns.query, dns.rdatatype

zone = dns.zone.from_xfr(dns.query.xfr("198.51.100.53", "example.com"))
for name, node in zone.nodes.items():
    for rdataset in node.rdatasets:
        print(name, rdataset.ttl, dns.rdatatype.to_text(rdataset.rdtype))

zone.to_file("example.com.zone")

Almost every public nameserver refuses AXFR, so expect a refused transfer unless you control the server or are on its allow list. Use dns.query.inbound_xfr for incremental IXFR against a zone you already have.

Add a record with an authenticated dynamic updatedynamic-update

import dns.update, dns.query, dns.tsigkeyring, dns.tsig

keyring = dns.tsigkeyring.from_text({"update-key.": "base64secret=="})
upd = dns.update.Update("example.com", keyring=keyring,
                        keyalgorithm=dns.tsig.HMAC_SHA256)
upd.replace("www", 300, "A", "203.0.113.10")
response = dns.query.tcp(upd, "198.51.100.53", timeout=10)
print(dns.rcode.to_text(response.rcode()))

replace overwrites the whole rrset, add appends to it, and delete removes. Send updates over TCP: a rejected update returns a rcode such as NOTAUTH rather than raising, so check it.

Validate a DNSSEC signaturevalidate-dnssec

import dns.dnssec, dns.message, dns.name, dns.query, dns.rdatatype

name = dns.name.from_text("example.com")
q = dns.message.make_query(name, dns.rdatatype.DNSKEY, want_dnssec=True)
resp = dns.query.udp(q, "8.8.8.8", timeout=5)
rrset, rrsig = resp.answer
dns.dnssec.validate(rrset, rrsig, {name: rrset})  # raises on failure

Requires pip install dnspython[dnssec]. This checks one signature against the keys you passed; it is not chain-of-trust validation up to the root, which you still have to build or delegate to a validating resolver.

Alternatives

PackageRegistryPick it when
aiodnsPyPIYou need thousands of concurrent lookups in asyncio and can live with c-ares limits; it is much faster per query but covers fewer record types.
dnslibPyPIYou are building a DNS server or proxy and mainly need to encode and decode wire-format packets rather than resolve names.
async-dnsPyPIYou want a small pure-Python asyncio client and server in one package and do not need DNSSEC, zone files, or dynamic updates.