Skip to content

/commands/dns/query-dns-records-python

Query DNS records in Python with dnspython (A, MX, TXT)

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash)updated
tool
DNS
versions
dnspython 2.x, Python 3.9+
os
any

To look up DNS records from Python, install dnspython and call dns.resolver.resolve(name, rtype). It returns an Answer you iterate over, one rdata per record, and raises dns.resolver.NXDOMAIN when the name does not exist and dns.resolver.NoAnswer when the name exists without that record type. To ask a specific server, create a Resolver(configure=False) and set nameservers (and port).

Example

The block starts a tiny DNS server on 127.0.0.1 port 5213 that serves a test zone, then queries it the way you would query a real resolver:

bash
cat > lookup.py <<'EOF'
import socket, threading
import dns.message, dns.rcode, dns.resolver, dns.rrset, dns.zone

zone = dns.zone.from_text("""
$TTL 300
@    IN SOA  ns1 hostmaster 1 3600 600 86400 300
@    IN NS   ns1
@    IN A    192.0.2.10
ns1  IN A    192.0.2.53
@    IN MX   10 mail
mail IN A    192.0.2.25
@    IN TXT  "v=spf1 mx -all"
""", origin="example.test.", relativize=False)

def serve(sock, count):
    for _ in range(count):
        data, addr = sock.recvfrom(4096)
        q = dns.message.from_wire(data)
        r = dns.message.make_response(q)
        qn = q.question[0]
        node = zone.get_node(qn.name)
        rds = node.get_rdataset(qn.rdclass, qn.rdtype) if node else None
        if rds:
            r.answer.append(dns.rrset.from_rdata_list(qn.name, rds.ttl, list(rds)))
        elif node is None:
            r.set_rcode(dns.rcode.NXDOMAIN)
        sock.sendto(r.to_wire(), addr)

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("127.0.0.1", 5213))
threading.Thread(target=serve, args=(s, 6), daemon=True).start()

res = dns.resolver.Resolver(configure=False)
res.nameservers = ["127.0.0.1"]
res.port = 5213
res.lifetime = 5

for rtype in ["A", "MX", "TXT"]:
    answer = res.resolve("example.test", rtype)
    for rdata in answer:
        print(rtype, "ttl", answer.rrset.ttl, rdata.to_text())

mx = sorted(res.resolve("example.test", "MX"), key=lambda r: r.preference)
print("mail goes to", mx[0].exchange)

for name, rtype in [("nope.example.test", "A"), ("mail.example.test", "AAAA")]:
    try:
        res.resolve(name, rtype)
    except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e:
        print(type(e).__name__, "-", e)
EOF
uv run -q --no-project --python 3.13 --with dnspython==2.8.0 python lookup.py

Output

A ttl 300 192.0.2.10
MX ttl 300 10 mail.example.test.
TXT ttl 300 "v=spf1 mx -all"
mail goes to mail.example.test.
NXDOMAIN - The DNS query name does not exist: nope.example.test.
NoAnswer - The DNS response does not contain an answer to the question: mail.example.test. IN AAAA

rdata.to_text() prints the record data in zone-file format; typed attributes are also available (preference and exchange for MX, strings for TXT, address for A and AAAA).

Common calls

CallWhat it does
dns.resolver.resolve('example.com', 'A')Uses the system configuration (resolv.conf, or the registry on Windows).
dns.resolver.resolve('example.com', 'AAAA')IPv6 addresses.
dns.resolver.resolve_address('192.0.2.10')Reverse (PTR) lookup; builds the in-addr.arpa name for you.
Resolver(configure=False) with nameservers=['1.1.1.1']Ignores the system configuration and asks that server.
res.timeout / res.lifetimeSeconds per server attempt / for the whole resolution.
resolve(name, 'TXT', raise_on_no_answer=False)Returns an empty Answer instead of raising NoAnswer.
resolve(name, 'A', tcp=True)Sends the query over TCP instead of UDP.

Errors you will meet

  • dns.resolver.NXDOMAIN: The DNS query name does not exist — see the NXDOMAIN page for causes such as typos and missing search domains.
  • dns.resolver.NoAnswer — the name exists but has no record of that type; for example a host with only an A record has no AAAA.
  • dns.resolver.LifetimeTimeout — no server answered within lifetime (default 5 seconds); check the server address, port and firewall.
  • dns.resolver.NoNameservers — every server returned an error such as SERVFAIL or REFUSED; the DNS RCODE table explains each code.

Names are treated as absolute: resolve('example.test') asks for example.test.; pass search=True to try the search domains from the system configuration.

FAQ

Why not use socket.getaddrinfo?

getaddrinfo returns only addresses (A and AAAA, plus the hosts file) and follows the operating system's resolver configuration. It cannot ask for MX, TXT, NS or SOA records or choose the server, which dnspython does.

What is the difference between resolve() and query()?

dnspython 2.0 renamed query() to resolve(). resolve() does not apply the search list from resolv.conf unless you pass search=True; query() is kept as a deprecated alias with the old behaviour.

How do I get the TTL of the answer?

The Answer object has rrset.ttl, or answer.rrset.ttl. It is the remaining TTL as reported by the server that answered, which for a caching resolver counts down over time.

Sources