aiodns: a small asyncio DNS resolver built on pycares
Simple DNS resolver for asyncio
At a glance
- What is it?
- aiodns wraps the pycares resolver in an asyncio API, and its 4.x line changed the shape of the results you get back, which is the one thing a caller has to understand before upgrading from 3.x.
- Who is it for?
- aiodns is the right choice for an asyncio application that needs genuine asynchronous name resolution instead of a thread per lookup, and for a team already using pycares that wants a coroutine wrapper without reimplementing the callback plumbing.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
pycares with an asyncio surface
aiodns is described as a simple DNS resolver for asyncio, and the whole implementation is a thin layer over pycares. The project provides a simple way of doing asynchronous DNS resolutions using pycares, and the dependency is declared in the manifest as pycares with a range that pins the major version.
That is a small package with a small job, and the design is deliberately narrow. The resolver object is created, you ask it questions, and you get futures back. It does not do caching, it does not do service discovery and it does not try to be a replacement for the resolver in your operating system.
The record types it supports are listed in the documentation: A, AAAA, ANY, CAA, CNAME, MX, NAPTR, NS, PTR, SOA, SRV and TXT. That list is the vocabulary of the library, and it is a DNS vocabulary rather than a web one, so the caller does the translating.
The package metadata is plain and gives a good picture of the support matrix. The project is MIT licensed, it requires Python 3.10 or newer, and the classifiers cover POSIX and Microsoft Windows with Python 3.10 through 3.14, and the development status is listed as Production/Stable. The author is Saúl Ibarra Corretgé, the version is taken dynamically from the attribute in the package, and the readme is the reStructuredText file you would expect to find on the package index page.
The build system requires both setuptools and pycares, with a comment explaining that pycares is needed because it is imported in the package's init module, which is a small but real detail about how the package is put together.
The API, and the two methods that are on the way out
The documentation says the API is pretty simple, and lists the functions provided in the DNSResolver class.
The current method is query_dns(host, type), which does a DNS resolution of the given type for the given hostname and returns an asyncio.Future. The result is a pycares.DNSResult object with answer, authority and additional attributes holding lists of pycares.DNSRecord objects, and each record has type, ttl and data attributes, with the shape of the data attribute depending on the record type.
Two older methods are marked deprecated. query(host, type) is deprecated in favour of query_dns, and it returns results in a legacy format compatible with aiodns 3.x for backward compatibility. gethostbyname(host, socket_family) is deprecated in favour of getaddrinfo, and it did a DNS resolution for a hostname and a desired address family such as socket.AF_INET.
The remaining methods are still current. gethostbyaddr(name) makes a reverse lookup for an address. getaddrinfo(host, family, port, proto, type, flags) resolves a host and port into a list of address info entries. getnameinfo(sockaddr, flags) resolves a socket address to a host and port. cancel() cancels all pending DNS queries, and every affected future gets a DNSError exception with the ARES_ECANCELLED errno. close() closes the resolver, releases all resources and cancels any pending queries.
Two details in close() deserve attention. It must be called when the resolver is no longer needed, for instance at application shutdown, and it should only be closed from the event loop that created it. Both are the kind of constraint that is easy to miss and awkward to debug, so they are worth reading before the first deployment rather than after it.
The result type change is the real migration
If you are upgrading from 3.x, the substantive change is not the method name, it is the shape of the object you iterate over.
aiodns 4.x introduces the query_dns method, which returns native pycares 5.x result types. The old query method is deprecated but keeps working, so the two can coexist while you migrate.
# Old API (deprecated)
result = await resolver.query('example.com', 'MX')
for record in result:
print(record.host, record.priority)
# New API (recommended)
result = await resolver.query_dns('example.com', 'MX')
for record in result.answer:
print(record.data.exchange, record.data.priority)The difference is visible in two places. The old call iterated the result directly, while the new one iterates result.answer, because the DNSResult object groups the answer, authority and additional sections separately. And the fields moved: what was record.host and record.priority on the legacy object is now record.data.exchange and record.data.priority, with the data attribute holding the type specific payload.
The project has also stated what comes next. The temporary query_dns naming allows gradual migration without breaking changes, and in aiodns 5.x query becomes the primary API returning native pycares 5.x types, with query_dns remaining as an alias for backward compatibility so that downstream projects can migrate at their own pace.
For a caller, the practical instruction is therefore to move to query_dns and read the records through result.answer and record.data, because that code will keep working when the alias is removed.
Long-lived resolvers, and a context manager you are told to avoid
The resolver is designed to be long lived and reused for many queries, and the documentation goes out of its way to say so, which is unusual but sensible.
A context manager is supported, and it closes the resolver automatically on exit.
async with aiodns.DNSResolver() as resolver:
result = await resolver.query_dns('example.com', 'A')
# resolver.close() is called automatically when exiting the contextThe documentation then discourages the pattern for most applications, because DNSResolver instances are designed to be long lived and reused for many queries, and creating and destroying resolvers frequently adds unnecessary overhead. The recommended use is the context manager for cases where automatic cleanup is genuinely wanted, such as tests or one-off scripts.
The general advice that follows from this is simple. Create one resolver, hold it for the life of the service, and close it during shutdown. Creating a resolver per lookup is the mistake the API is nudging you away from, and since the resolver is cheap to create but not free, a per request pattern turns into a per request cost in sockets and c-ares channels.
Cancel is the other half of the lifecycle story, and it is blunt. It cancels all pending queries at once and every future is completed with a DNSError carrying the ARES_ECANCELLED errno, so a caller has to be prepared to handle that exception on outstanding awaits when the resolver is torn down or a batch of queries is abandoned.
The Windows event loop caveat, and how to avoid it
There is a section of the documentation devoted to Windows users, and the condition is narrow enough that it is easy to check.
The library requires the use of anything but a ProactorEventLoop on Windows, and only when using a custom build of pycares that links against a system provided c-ares library without thread-safety support. The reason given is that non thread safe builds of c-ares are incompatible with the default ProactorEventLoop, because ProactorEventLoop does not implement an add_reader or add_writer function for pycares to safely traverse its own callbacks with.
If you are using the official prebuilt pycares wheels on PyPI, version 4.7.0 or later, which include a thread safe version of c-ares, the limitation does not apply and can be safely ignored. That is the common case, and it means a Windows user on a normal pip install has nothing to do.
If you do hit it, the documented fix is to change the default event loop, and to do it very early in the application.
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())The documentation warns that this may have other implications for the rest of the codebase, so test thoroughly. That is the correct advice rather than a formality, because the selector event loop is a different implementation with different characteristics from the proactor one, and any code that depends on proactor specific behaviour will notice.
The dependency pinning in the manifest is what makes this rare: the runtime requirement is pycares with a major version range, and the development requirements pin a specific pycares release, a specific pytest, and the event loop alternatives, uvloop on non Windows CPython and winloop on Windows.
Running the tests and the packaging around the package
The test suite is run with a single command, python -m pytest tests/, and the development requirements show how it is assembled. Besides the editable install of the package itself and the pinned pycares, there is pytest, pytest-asyncio, pytest-cov for coverage, and the event loop packages, uvloop for non Windows CPython and winloop for Windows with two versions depending on the Python version.
The repository layout is a mature Python project rather than a script. There is a pyproject.toml with the packaging metadata, a setup.cfg, a MANIFEST.in, a pytest.ini, a .coveragerc and a .codecov.yml for coverage reporting, a .mypy.ini for type checking, a .ruff.toml for linting, a .pre-commit-config.yaml for the pre-commit hooks, a scripts directory, a tests directory, the aiodns package itself and a ChangeLog file.
The packaging configuration contains one choice worth flagging. The setuptools configuration disables inclusion of non-package data such as tests in the wheel when it is present in the source distribution, and the package discovery excludes the tests directories explicitly, so a wheel contains the library and not the test suite.
The ChangeLog is a real file at the root rather than a section of the readme, which matters for the release process below.
Releases are cut from master by a workflow
The maintenance and release process is documented in detail, which is a good sign for a library that other projects depend on.
Releases are cut from master and published to PyPI automatically by the Release Wheels workflow when a GitHub Release is created. The first step is to prepare a release pull request: bump the version in the package init file and prepend a section to the ChangeLog describing the user facing changes since the previous tag, in the same reStructuredText style as the existing entries, with the version header underlined. The pull request is titled with the release version and merged once continuous integration is green.
There is a dependency policy in the same section. Dependabot bumps for development tooling and CI actions are to be skipped, while runtime dependency bumps such as pycares are to be kept, which tells you the maintainer treats the runtime dependency line as something to track deliberately rather than accept automatically.
The version history shows the recent pace. Version 4.0.4 was published on 2026-05-20, and 4.0.3 and 4.0.2 both on 2026-05-16, which looks like a burst of fixes on the 4.0 line. The last push to the default branch was on 2026-10-19, so work is landing on master even when no release follows immediately, and the repository is not archived.
The licence is MIT, with a LICENSE file at the root, so the code can be used, modified and redistributed in commercial products with the notice retained, and that is not legal advice. For a package this small and this widely depended upon, the combination of a permissive licence, pinned development tooling and a documented release procedure is the reassuring part.
Editorial conclusion
aiodns is the right choice for an asyncio application that needs genuine asynchronous name resolution instead of a thread per lookup, and for a team already using pycares that wants a coroutine wrapper without reimplementing the callback plumbing. It is the wrong choice for a synchronous service, for a project that only needs to resolve one name at startup, and for a Windows application that has already committed to the default Proactor event loop with a non thread safe c-ares build. Before adopting, check that your callers can live with the pycares 5.x record objects, that you call close() from the event loop that created the resolver, and that on Windows you are using the prebuilt pycares wheels rather than a custom build against the system library.
Frequently asked questions
What does aiodns do and what does it depend on?
It provides asynchronous DNS resolution for asyncio by wrapping the pycares library, and the runtime dependency is declared as pycares with a major version range of 5. The package requires Python 3.10 or newer and is MIT licensed.
Which DNS record types does aiodns support?
The documented list is A, AAAA, ANY, CAA, CNAME, MX, NAPTR, NS, PTR, SOA, SRV and TXT. The type specific payload of each record is available through the data attribute, with the details in the pycares documentation.
How do I migrate from aiodns 3.x to 4.x?
Use query_dns instead of the deprecated query method, iterate result.answer rather than the result itself, and read type specific fields through record.data. The old query method still works in a format compatible with aiodns 3.x, and in 5.x the naming is planned to flip so that query becomes primary and query_dns becomes the alias.
When should I call close() on the aiodns resolver?
When the resolver is no longer needed, for example at application shutdown, and only from the event loop that created it. The resolver is designed to be long lived and reused for many queries, and the documentation discourages the async context manager pattern for most applications because creating resolvers frequently adds unnecessary overhead.
Does aiodns have a problem with the Windows Proactor event loop?
Only when using a custom build of pycares linked against a system provided c-ares without thread-safety support, because the ProactorEventLoop does not implement add_reader or add_writer. The official prebuilt pycares wheels on PyPI, version 4.7.0 or later, include a thread safe c-ares and the limitation does not apply.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/aio-libs-aiodns)Was this useful?