Simple DNS resolver for asyncio
npx @tessl/cli install tessl/pypi-aiodns@3.5.0Simple DNS resolver for asyncio that provides asynchronous DNS resolution capabilities using the pycares library. aiodns enables non-blocking DNS queries in Python applications using async/await syntax, supporting all major DNS record types and offering efficient hostname resolution with /etc/hosts support.
pip install aiodnspycares>=4.9.0import aiodnsCommon usage pattern:
from aiodns import DNSResolverAccess to error constants and types:
from aiodns import error
from aiodns.error import DNSError, ARES_ENOTFOUND, ARES_ETIMEOUT
import socket
import asyncio
from types import TracebackType
from typing import Optional, Sequence, Union, Iterable, Literal, Any
import pycaresimport asyncio
import aiodns
async def main():
# Create a DNS resolver
resolver = aiodns.DNSResolver()
try:
# Perform A record query (returns list[pycares.ares_query_a_result])
a_result = await resolver.query('google.com', 'A')
print(f"A records: {[r.host for r in a_result]}")
# Perform AAAA record query (returns list[pycares.ares_query_aaaa_result])
aaaa_result = await resolver.query('google.com', 'AAAA')
print(f"IPv6 addresses: {[r.host for r in aaaa_result]}")
# CNAME query returns single result (not a list)
cname_result = await resolver.query('www.github.com', 'CNAME')
print(f"CNAME: {cname_result.cname}")
# MX query returns list of results
mx_result = await resolver.query('github.com', 'MX')
print(f"MX records: {[(r.host, r.priority) for r in mx_result]}")
# Hostname resolution with /etc/hosts support
import socket
host_result = await resolver.gethostbyname('localhost', socket.AF_INET)
print(f"Localhost IP: {host_result.addresses}")
except aiodns.error.DNSError as e:
print(f"DNS error: {e}")
finally:
# Clean up resources
await resolver.close()
# Using async context manager (automatic cleanup)
async def context_example():
async with aiodns.DNSResolver() as resolver:
result = await resolver.query('example.com', 'A')
print(f"IP addresses: {[r.host for r in result]}")
# resolver.close() called automatically
asyncio.run(main())Perform DNS queries for various record types with full pycares integration.
class DNSResolver:
def __init__(
self,
nameservers: Optional[Sequence[str]] = None,
loop: Optional[asyncio.AbstractEventLoop] = None,
**kwargs: Any
) -> None:
"""
Create a DNS resolver instance.
Parameters:
- nameservers: Optional list of DNS server IP addresses
- loop: Optional asyncio event loop (defaults to current loop)
- **kwargs: Additional options passed to pycares.Channel
"""
# Type-specific query method overloads
def query(self, host: str, qtype: Literal['A'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_a_result]]:
"""Query A records (IPv4 addresses)."""
def query(self, host: str, qtype: Literal['AAAA'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_aaaa_result]]:
"""Query AAAA records (IPv6 addresses)."""
def query(self, host: str, qtype: Literal['CAA'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_caa_result]]:
"""Query CAA records (Certificate Authority Authorization)."""
def query(self, host: str, qtype: Literal['CNAME'], qclass: Optional[str] = None) -> asyncio.Future[pycares.ares_query_cname_result]:
"""Query CNAME record (Canonical Name) - returns single result."""
def query(self, host: str, qtype: Literal['MX'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_mx_result]]:
"""Query MX records (Mail Exchange)."""
def query(self, host: str, qtype: Literal['NAPTR'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_naptr_result]]:
"""Query NAPTR records (Name Authority Pointer)."""
def query(self, host: str, qtype: Literal['NS'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_ns_result]]:
"""Query NS records (Name Server)."""
def query(self, host: str, qtype: Literal['PTR'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_ptr_result]]:
"""Query PTR records (Pointer)."""
def query(self, host: str, qtype: Literal['SOA'], qclass: Optional[str] = None) -> asyncio.Future[pycares.ares_query_soa_result]:
"""Query SOA record (Start of Authority) - returns single result."""
def query(self, host: str, qtype: Literal['SRV'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_srv_result]]:
"""Query SRV records (Service)."""
def query(self, host: str, qtype: Literal['TXT'], qclass: Optional[str] = None) -> asyncio.Future[list[pycares.ares_query_txt_result]]:
"""Query TXT records (Text)."""
# Generic query method for runtime use
def query(self, host: str, qtype: str, qclass: Optional[str] = None) -> Union[asyncio.Future[list[Any]], asyncio.Future[Any]]:
"""
Perform DNS query for specified record type.
Parameters:
- host: Hostname or domain to query
- qtype: Query type ('A', 'AAAA', 'CNAME', 'MX', 'TXT', 'PTR', 'SOA', 'SRV', 'NS', 'CAA', 'NAPTR', 'ANY')
- qclass: Query class ('IN', 'CHAOS', 'HS', 'NONE', 'ANY'), defaults to 'IN'
Returns:
- asyncio.Future with query results (type varies by query type)
- Most queries return list[ResultType], but CNAME and SOA return single ResultType
Raises:
- ValueError: Invalid query type or class
- DNSError: DNS resolution errors
"""Resolve hostnames to IP addresses with /etc/hosts file support.
def gethostbyname(
self,
host: str,
family: socket.AddressFamily
) -> asyncio.Future[pycares.ares_host_result]:
"""
Resolve hostname to IP address, checking /etc/hosts first.
Parameters:
- host: Hostname to resolve
- family: Address family (socket.AF_INET or socket.AF_INET6)
Returns:
- asyncio.Future[pycares.ares_host_result] with host information
"""
def getaddrinfo(
self,
host: str,
family: socket.AddressFamily = socket.AF_UNSPEC,
port: Optional[int] = None,
proto: int = 0,
type: int = 0,
flags: int = 0
) -> asyncio.Future[pycares.ares_addrinfo_result]:
"""
Get address information for hostname (async equivalent of socket.getaddrinfo).
Parameters:
- host: Hostname to resolve
- family: Address family (socket.AF_INET, socket.AF_INET6, or socket.AF_UNSPEC)
- port: Port number (optional)
- proto: Protocol (0 for any)
- type: Socket type (0 for any)
- flags: Additional flags
Returns:
- asyncio.Future[pycares.ares_addrinfo_result] with address information
"""Perform reverse DNS lookups from IP addresses to hostnames.
def gethostbyaddr(self, name: str) -> asyncio.Future[pycares.ares_host_result]:
"""
Perform reverse DNS lookup from IP address to hostname.
Parameters:
- name: IP address string to resolve
Returns:
- asyncio.Future[pycares.ares_host_result] with hostname information
"""
def getnameinfo(
self,
sockaddr: Union[tuple[str, int], tuple[str, int, int, int]],
flags: int = 0
) -> asyncio.Future[pycares.ares_nameinfo_result]:
"""
Reverse name resolution from socket address.
Parameters:
- sockaddr: Socket address tuple (IPv4: (host, port), IPv6: (host, port, flow, scope))
- flags: Additional flags
Returns:
- asyncio.Future[pycares.ares_nameinfo_result] with name information
"""Control resolver lifecycle and cancel operations.
@property
def nameservers(self) -> Sequence[str]:
"""Get current DNS nameservers."""
@nameservers.setter
def nameservers(self, value: Iterable[Union[str, bytes]]) -> None:
"""Set DNS nameservers."""
def cancel(self) -> None:
"""Cancel all pending DNS queries. All futures will receive DNSError with ARES_ECANCELLED."""
async def close(self) -> None:
"""
Cleanly close the DNS resolver and release all resources.
Must be called when resolver is no longer needed.
"""
def __del__(self) -> None:
"""
Handle cleanup when the resolver is garbage collected.
Automatically calls _cleanup() to release resources if resolver was not properly closed.
Note: Explicit close() is preferred over relying on garbage collection.
"""Automatic resource cleanup using async context manager pattern.
async def __aenter__(self) -> 'DNSResolver':
"""Enter async context manager."""
async def __aexit__(
self,
exc_type: Optional[type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType]
) -> None:
"""Exit async context manager, automatically calls close()."""DNS errors and status codes from the underlying pycares library.
class DNSError(Exception):
"""Base class for all DNS errors."""
# Error constants (integers)
ARES_SUCCESS: int
ARES_ENOTFOUND: int # Domain name not found
ARES_EFORMERR: int # Format error
ARES_ESERVFAIL: int # Server failure
ARES_ENOTINITIALIZED: int # Not initialized
ARES_EBADNAME: int # Bad domain name
ARES_ETIMEOUT: int # Timeout
ARES_ECONNREFUSED: int # Connection refused
ARES_ENOMEM: int # Out of memory
ARES_ECANCELLED: int # Query cancelled
ARES_EBADQUERY: int # Bad query
ARES_EBADRESP: int # Bad response
ARES_ENODATA: int # No data
ARES_EBADFAMILY: int # Bad address family
ARES_EBADFLAGS: int # Bad flags
ARES_EBADHINTS: int # Bad hints
ARES_EBADSTR: int # Bad string
ARES_EREFUSED: int # Refused
ARES_ENOTIMP: int # Not implemented
ARES_ESERVICE: int # Service error
ARES_EFILE: int # File error
ARES_EOF: int # End of file
ARES_EDESTRUCTION: int # Destruction
ARES_ELOADIPHLPAPI: int # Load IP helper API error
ARES_EADDRGETNETWORKPARAMS: int # Address get network params errorQuery result types vary by DNS record type:
# A record results
pycares.ares_query_a_result:
host: str # IPv4 address
# AAAA record results
pycares.ares_query_aaaa_result:
host: str # IPv6 address
# CNAME record result
pycares.ares_query_cname_result:
cname: str # Canonical name
# MX record results
pycares.ares_query_mx_result:
host: str # Mail server hostname
priority: int # Priority value
# TXT record results
pycares.ares_query_txt_result:
text: bytes # Text record data
# PTR record results
pycares.ares_query_ptr_result:
name: str # Pointer target name
# SOA record result
pycares.ares_query_soa_result:
nsname: str # Primary nameserver
hostmaster: str # Responsible person email
serial: int # Serial number
refresh: int # Refresh interval
retry: int # Retry interval
expires: int # Expiry time
minttl: int # Minimum TTL
# SRV record results
pycares.ares_query_srv_result:
host: str # Target hostname
port: int # Port number
priority: int # Priority
weight: int # Weight
# NS record results
pycares.ares_query_ns_result:
host: str # Nameserver hostname
# CAA record results
pycares.ares_query_caa_result:
critical: int # Critical flag
property: bytes # Property name
value: bytes # Property value
# NAPTR record results
pycares.ares_query_naptr_result:
order: int # Order value
preference: int # Preference value
flags: bytes # Flags
service: bytes # Service
regexp: bytes # Regular expression
replacement: bytes # Replacement
# Host resolution results
pycares.ares_host_result:
name: str # Primary hostname
aliases: List[str] # Hostname aliases
addresses: List[str] # IP addresses
# Address info results
pycares.ares_addrinfo_result:
nodes: List[ares_addrinfo_node] # Address information nodes
# Address info node
pycares.ares_addrinfo_node:
family: int # Address family (socket.AF_INET, socket.AF_INET6)
socktype: int # Socket type
protocol: int # Protocol
addr: tuple # Address tuple (host, port) or (host, port, flow, scope)
canonname: str # Canonical name (optional)
# Name info results
pycares.ares_nameinfo_result:
node: str # Hostname
service: str # Service nameaiodns requires asyncio.SelectorEventLoop or winloop on Windows when using custom pycares builds without thread-safety. Official PyPI wheels (pycares 4.7.0+) include thread-safe c-ares and work with any event loop.
# For non-thread-safe pycares builds on Windows:
import asyncio
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())aiodns automatically detects thread-safe pycares builds and optimizes accordingly: