CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-aiodns

Simple DNS resolver for asyncio

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

aiodns

Simple 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.

Package Information

  • Package Name: aiodns
  • Version: 3.5.0
  • Language: Python
  • Installation: pip install aiodns
  • Dependencies: pycares>=4.9.0
  • Python Version: 3.9+

Core Imports

import aiodns

Common usage pattern:

from aiodns import DNSResolver

Access 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 pycares

Basic Usage

import 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())

Capabilities

DNS Query Operations

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
        """

Hostname Resolution

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
    """

Reverse DNS Lookup

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
    """

Resource Management

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.
    """

Async Context Manager Support

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()."""

Error Handling

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 error

Types

Query 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 name

Platform Notes

Windows Compatibility

aiodns 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())

Thread Safety

aiodns automatically detects thread-safe pycares builds and optimizes accordingly:

  • Thread-safe builds: Uses pycares event thread for better performance
  • Non-thread-safe builds: Falls back to socket state callbacks with asyncio integration

Install with Tessl CLI

npx tessl i tessl/pypi-aiodns
Workspace
tessl
Visibility
Public
Created
Last updated
Describes
pypipkg:pypi/aiodns@3.5.x
Publish Source
CLI
Badge
tessl/pypi-aiodns badge