1.180.1 Python Database Drivers#
Python database drivers below the ORM: install path and interface shape decide the choice; speed differs on large fetches and bulk writes, not single queries.
Evidence: measured, method not yet published. The numbers here were produced by probes run on a stated machine; the harness is not in the public repository yet, so a reader cannot re-run them.
At a glance#
| Library | Type | Verdict | Latest release |
|---|---|---|---|
| psycopg 3 | DB-API, C accelerator or pure Python, libpq | Default for new PostgreSQL work; sync, async and pool (psycopg_pool) | 3.3.6 · 2026-09-18 |
| psycopg2 | DB-API, C binding to libpq | Maintained; source build advised for production; plain executemany is one round trip per row | 2.9.13 · 2026-09-09 |
| asyncpg | Own API, Cython, own protocol implementation | Fastest async PostgreSQL path; not DB-API; releases slow, main active | 0.31.0 · 2025-11-24 |
| pg8000 | DB-API, pure Python | Installs anywhere; slowest measured; moved to Codeberg 2025-09 | 1.31.5 · 2025-09-14 |
| mysqlclient | DB-API, C binding to libmysqlclient | Windows wheels only; source build needs headers; GPL-2.0-or-later | 2.3.0 · 2026-09-14 |
| PyMySQL | DB-API, pure Python | Zero-install MySQL driver; about 4x slower than C on large fetches | 1.2.3 · 2026-09-17 |
| aiomysql | Awaitable cursor interface over PyMySQL protocol code | Stall signal: 35 open PRs, none merged since 2026-04-05 | 0.3.2 · 2025-10-22 |
| asyncmy | Awaitable cursor interface, Cython | Active async MySQL driver with 64 wheels; fastest large fetch measured | 0.2.15 · 2026-09-21 |
| sqlite3 | DB-API, C module in the standard library | Default for SQLite; 6.6 microseconds per point query measured | — |
| aiosqlite | Awaitable cursor interface, thread-backed wrapper of sqlite3 | Stall signal: 16 open PRs, none merged; about 36x slower per point query than sqlite3 | 0.22.1 · 2025-12-23 |
Latest release observed from PyPI in 2026-10.
What the research found
- psycopg2 and mysqlclient publish no Linux or macOS wheel under their own names; a source build failed locally for both on missing development headers. — In slim containers the first failure is the build, which is why psycopg2-binary and psycopg[binary] exist.
- psycopg2’s documentation calls psycopg2-binary meant for beginners and advises the source distribution for production, citing bundled libpq and libssl that miss system updates and can conflict with other OpenSSL users. — Binary wheels suit images rebuilt on base-image updates; long-lived hosts should build from source or use a pure-Python driver.
- Single-row queries were a tie among the C and Cython drivers (529 to 934 microseconds on a loopback server); 100,000-row fetches ran four to nine times faster compiled than pure Python. — Choose a compiled driver for large result sets, not for small-query latency.
- executemany for 20,000 rows took 11.2 s in psycopg2 and 37.6 s in pg8000 against 0.21 s in psycopg 3 and 0.16 s in asyncpg. — Plain DB-API bulk insert in psycopg2 costs one round trip per row; psycopg 3 pipelines, and psycopg 3’s async executemany was an unexplained 1.1 s.
- aiomysql (35 open PRs, none merged since 2026-04-05) and aiosqlite (16 open, none merged) show stall signals; asyncpg’s last release was 2025-11-24 while main is active. — Plan a replacement path for the two wrappers and a release-lag fallback for asyncpg.
What the research recommends
Default — PostgreSQL: psycopg 3 (binary in rebuilt containers, source-built C on shared hosts); asyncpg when async-only and PostgreSQL-only. MySQL: PyMySQL (sync), asyncmy (async). SQLite: sqlite3.
| Persona | Pick |
|---|---|
| web app migrating psycopg2 | psycopg 3; expect server-side binding breaks; bulk writes gain the most |
| high throughput async service | asyncpg for PostgreSQL-only speed; psycopg 3 async plus psycopg_pool for DB-API or mixed sync/async; decide pooler interaction first |
| sqlite data script | sqlite3; aiosqlite only inside an existing asyncio program |
| mysql shop | asyncmy and PyMySQL; mysqlclient where headers are managed; watch aiomysql |
| minimal image | PyMySQL, pg8000, sqlite3; psycopg pure Python if libpq is present |
Explainer
What Is a Python Database Driver?#
The hardware store version#
An ORM (object-relational mapper) is the power tool with a dozen attachments. A driver is the chuck that holds the bit: the part that physically connects the tool to the work. Nobody shops for chucks until the drill will not turn, and then the choice matters a great deal: a wrong one means the bit slips, or the drill only fits one brand of bit, or it works in the shop and fails on the job site.
A database driver is the Python package that opens a connection to a database server, sends SQL, and turns the server’s reply into Python objects. Every ORM, query builder and migration tool sits on one. The ORM survey (1.180) covers the layer above; this survey covers the layer below.
The problem it solves#
A database server speaks a wire protocol: PostgreSQL’s frontend/backend protocol, MySQL’s client protocol, or, for SQLite, no protocol at all because the engine runs inside your process. Something has to speak it. There are three ways a Python package can:
- Wrap the vendor’s C client library.
libpqfor PostgreSQL,libmysqlclientor MariaDB Connector/C for MySQL. The driver is a thin Python binding. Fast and complete, but the C library has to be present at build time, and often at run time. - Implement the protocol itself in Python. No C library, no compiler, installs anywhere a wheel is allowed. Slower on large result sets, and the maintainers own every protocol corner.
- Implement the protocol in a compiled extension of its own. asyncpg and asyncmy take this route: the speed of C without depending on the vendor’s client library.
Which of the three a driver uses decides how it installs, how fast it runs, what license applies and how it breaks.
The contract underneath: PEP 249#
Most synchronous drivers follow PEP 249, the Python Database API Specification v2.0
(Marc-Andre Lemburg, 2001, still the current revision). It defines connect(), connection and
cursor objects, execute, executemany, fetchone/fetchmany/fetchall, commit/rollback,
an exception hierarchy, and three module constants that matter in practice: apilevel,
threadsafety and paramstyle (how ?, %s, :name placeholders are written). Source:
https://peps.python.org/pep-0249/.
PEP 249 predates async. It says nothing about coroutines, so every async driver invents its own
shape: some mimic the DB-API with await in front of each call (aiomysql, aiosqlite, psycopg’s
async classes), and one (asyncpg) drops the DB-API and exposes its own interface. That is the first
fork in the road and it decides how portable code written against the driver is.
Why the choice is not trivial#
- The same database has several drivers that disagree. PostgreSQL alone has psycopg2, psycopg 3, asyncpg and pg8000, with different parameter syntax, different transaction defaults and different behavior behind a connection pooler.
- Installation is part of the product. Two of the most-used drivers (psycopg2 and mysqlclient) publish binary wheels for Windows only on their main package. On Linux and macOS a plain install compiles C against headers that may not be there.
- The async split is real. A coroutine cannot call a blocking driver without stalling the event loop, so async services need an async driver or a thread pool.
- Licenses differ. psycopg is LGPL-3.0, mysqlclient is GPL-2.0-or-later, PyMySQL and aiomysql are MIT, asyncpg and asyncmy are Apache-2.0 (PyPI metadata, accessed 2026-10-05).
When you do not need to choose#
If an ORM or framework picks the driver for you (Django’s PostgreSQL backend, SQLAlchemy’s default dialect), the choice is already made and swapping it is a configuration line. Choose actively when you write raw SQL, when you run an async service, when you deploy somewhere without a compiler, or when you sit behind a pooler such as pgBouncer (see 1.184 Connection Pool Libraries).
How to read a driver’s name#
Python database packages carry naming conventions that hide the install story:
- A bare name (
psycopg2,mysqlclient) usually means “compile me”. It wraps a C library, and the build needs that library’s development headers. - A
-binarysuffix or extra (psycopg2-binary,psycopg[binary]) means “the compiled parts and a private copy of the C library are already inside”. Quick to install, but the private copy does not receive your operating system’s updates. - An
a-prefixed orasyncname (asyncpg,aiomysql,aiosqlite) means awaitable calls. Some are fresh implementations and some are wrappers around a sync driver, which changes both their speed and their durability. - A year-like or generation number (psycopg2 against psycopg 3) names a different codebase, not a version of the same one. They coexist on PyPI under different import names.
Where the costs show up#
Query speed is rarely what hurts. A driver spends most of its time waiting on the network, so a faster parser saves little on a one-row lookup. The costs that show up in production are different: a deployment that cannot build the driver, a library that cannot be patched independently of the image, a bulk insert that makes one network trip per row, or a connection count that exhausts the server because three layers each keep a pool. This survey measures the first two and flags the rest.
What this survey covers#
PostgreSQL (psycopg2, psycopg 3 with its pool, asyncpg, pg8000), MySQL and MariaDB (mysqlclient,
PyMySQL, aiomysql, asyncmy), and SQLite (stdlib sqlite3, aiosqlite). Oracle, SQL Server, and
ORMs are out of scope.
Sources#
- PEP 249: https://peps.python.org/pep-0249/ (the contract and its constants)
- psycopg 3 documentation, “Differences from psycopg2”: https://www.psycopg.org/psycopg3/docs/basic/from_pg2.html (server-side binding, the framing used for the binding discussion)
- asyncpg FAQ: https://magicstack.github.io/asyncpg/current/faq.html (pgBouncer and prepared statements)
S1: Rapid Discovery
S1 Rapid Discovery: Approach#
What the category is#
A Python database driver connects a Python process to one database server and exchanges SQL and rows with it. It sits below ORMs (1.180 Python ORMs), query builders and connection pools (1.184 Connection Pool Libraries), and above the operating system’s sockets.
Inclusion test: the package’s primary job is to speak to the database, it is installable from PyPI, and it was released or committed to in the last 12 months or is the standard-library module.
In scope#
| Database | Sync | Async |
|---|---|---|
| PostgreSQL | psycopg2, psycopg 3, pg8000 | psycopg 3 (async classes), asyncpg |
| MySQL / MariaDB | mysqlclient, PyMySQL | aiomysql, asyncmy |
| SQLite | sqlite3 (standard library) | aiosqlite |
Mentioned in S2 and S4 but not profiled: mysql-connector-python (Oracle’s driver) and the
mariadb connector (MariaDB Corporation’s), because they are first-party drivers with the same
shape as the profiled ones.
Out of scope#
ORMs, query builders, migration tools, connection poolers that run as separate processes, and drivers for other databases (Oracle, SQL Server, DuckDB, document and graph stores).
Evaluation dimensions#
- Interface: PEP 249 DB-API, or its own API.
- Implementation: C binding to the vendor library, pure Python, or its own compiled extension.
- Install path: which platforms get a binary wheel, and what a source build needs.
- Async story: none, a separate package, or native.
- License.
- Durability: release cadence and the stall test (open pull requests against merged ones).
- Cost: install footprint, import time and round-trip speed, measured locally.
Reading the profiles#
Versions and release dates come from the PyPI JSON API, accessed 2026-10-05 (source: https://pypi.org/pypi/psycopg/json and the sibling pages for each package). Repository figures come from the GitHub search API on the same date.
S1: MySQL and MariaDB Drivers#
Four profiled drivers, two of them async. Two first-party drivers are noted at the end.
At a glance#
| Driver | Latest | Released | Interface | Implementation | Async | License |
|---|---|---|---|---|---|---|
| mysqlclient | 2.3.0 | 2026-09-14 | DB-API (MySQLdb) | C binding to libmysqlclient or MariaDB Connector/C | No | GPL-2.0-or-later |
| PyMySQL | 1.2.3 | 2026-09-17 | DB-API | Pure Python | No | MIT |
| aiomysql | 0.3.2 | 2025-10-22 | DB-API-like, awaitable | Built on PyMySQL | Yes | MIT |
| asyncmy | 0.2.15 | 2026-09-21 | DB-API-like, awaitable | Cython, fork of aiomysql and PyMySQL ideas | Yes | Apache-2.0 |
(source: PyPI JSON API pages for each package, accessed 2026-10-05)
mysqlclient#
A maintained fork of the old MySQLdb, imported as MySQLdb. It wraps the client library, so it
is a thin, fast binding that tracks the server’s features.
Install friction is its defining trait. All 16 wheels on PyPI for 2.3.0 are Windows wheels; Linux
and macOS install from source and need the client library headers plus pkg-config or manual
MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS (source: PyPI file list, accessed 2026-10-05; the
failure was reproduced locally, see S2). The license is GPL-2.0-or-later, which matters to anyone
distributing a closed-source application that bundles it.
Repository: 1 open pull request, 19 merged since 2026-04-05 (source: GitHub search, accessed 2026-10-05). Maintained under the PyMySQL organization.
PyMySQL#
Pure-Python MySQL protocol, MIT licensed, one universal wheel. Because it needs nothing native it is the default choice where a compiler or client library is unavailable, and it is the base for aiomysql and several framework integrations. It supports MariaDB as well as MySQL. Local measurements put its large-result fetch at about three times the time of the C and Cython drivers (S2).
Repository: 4 open pull requests, 35 merged since 2026-04-05 (source: GitHub search, accessed 2026-10-05). Active.
aiomysql#
An asyncio layer over PyMySQL’s protocol code, DB-API shaped (await cursor.execute(...)), with a
pool. Under the aio-libs organization.
Stall signal. The repository has 35 open pull requests and none merged since 2026-04-05; the default branch has no commit in 2026, and the last human-authored release is 0.3.2 on 2025-10-22, with only dependency-bot commits after it (source: GitHub search and commits API, accessed 2026-10-05). People are still sending patches and nobody is merging them. That is a durability signal, not a quality verdict: the code works in the local measurements.
asyncmy#
An async driver that compiles the hot paths with Cython. Compatible with PyMySQL and aiomysql in shape, and the faster of the two async MySQL drivers in the large-fetch measurement (S2). It ships 64 wheels covering Linux (including aarch64 and musl), macOS and Windows. Repository: 0 open pull requests, 8 merged since 2026-04-05, 391 stars (source: GitHub search, accessed 2026-10-05). Active but a one-organization project; see S4.
First-party drivers not profiled#
- mysql-connector-python (Oracle), 26.7.0 on 2026-07-29, GPL-2.0 with the FOSS license exception, wheels for Linux, macOS and Windows. Offers a pure-Python and a C implementation. Install footprint is the largest of the MySQL options (S2).
- mariadb (MariaDB Corporation), 1.1.14 on 2025-10-07, LGPL-2.1, wheels for Windows only, needs MariaDB Connector/C elsewhere. 5 merged pull requests since 2026-04-05, 2 open. (source: PyPI and GitHub search, accessed 2026-10-05)
Cross-cutting facts#
- MariaDB. All four profiled drivers talk to MariaDB; mysqlclient can link MariaDB Connector/C.
- Authentication. MySQL 8’s default
caching_sha2_passwordneeds either TLS or an RSA key exchange; PyMySQL and aiomysql need thecryptographypackage for it. The local aiomysql run installed it explicitly. - Which driver an ORM uses. See 1.180 Python ORMs for the dialect names; pooling is covered in 1.184.
S1: PostgreSQL Drivers#
Four drivers, three implementation styles, two interface styles.
At a glance#
| Driver | Latest | Released | Interface | Implementation | Async | License |
|---|---|---|---|---|---|---|
| psycopg2 | 2.9.13 | 2026-09-09 | DB-API | C binding to libpq | No | LGPL with exceptions |
| psycopg 3 | 3.3.6 | 2026-09-18 | DB-API (with extensions) | Python, optional C or binary accelerator, libpq | Yes | LGPL-3.0 |
| asyncpg | 0.31.0 | 2025-11-24 | Own API | Cython, own protocol implementation | Native only | Apache-2.0 |
| pg8000 | 1.31.5 | 2025-09-14 | DB-API | Pure Python | No | BSD-3-Clause |
(source: https://pypi.org/pypi/psycopg/json and sibling pages for each package, accessed 2026-10-05)
psycopg2#
The long-standing default. A C extension over libpq, DB-API compliant, supporting COPY, large
objects, LISTEN/NOTIFY, server-side (named) cursors and a large extras module. Parameters are
interpolated on the client and sent as a finished SQL string.
It is still released (2.9.13 on 2026-09-09, requires Python 3.10 or newer) and still merging patches (6 open pull requests, 6 merged since 2026-04-05; source: GitHub search, accessed 2026-10-05). It is the older of the two psycopg generations; psycopg 3 is where new features land.
Install: the main psycopg2 package ships Windows wheels only on PyPI (6 of 7 files, rest is
the source distribution; source: PyPI file list, accessed 2026-10-05). On Linux and macOS it
compiles against libpq headers and a C compiler. psycopg2-binary carries the wheels for Linux,
macOS and Windows, and the maintainers say it is “meant for beginners” and that “for production
use you are advised to use the source distribution” (source:
https://www.psycopg.org/docs/install.html, accessed 2026-10-05). The stated reasons: the wheels bundle their own libpq and libssl, so upgrading the system
libraries does not upgrade them, and the bundled libssl can conflict with other extension
modules, including Python’s ssl module, to the point of a segfault under concurrency in some
cases (same page).
psycopg 3#
A ground-up rewrite, imported as psycopg. Same DB-API surface with a different engine:
- Server-side binding. Query and parameters travel separately, as PostgreSQL’s extended
protocol allows. This removes client-side quoting and makes prepared statements automatic after a
threshold; it also forbids several statements in one
executewhen parameters are used and does not work forSETorNOTIFY(source: psycopg 3 docs, “Differences from psycopg2”, accessed 2026-10-05). - Native async.
AsyncConnectionandAsyncCursorsit beside the sync classes. - Pipeline mode, COPY as an iterator, typed adapters, row factories.
- Three implementations of the same package: pure Python (needs a system
libpq), the C acceleratorpsycopg[c](built from source, needs headers), andpsycopg[binary](prebuilt C accelerator with its ownlibpq). The docs call the binary route “recommended for most users” and the equivalent of psycopg2-binary. They also state that binary packages are “produced on a best-effort basis” and are not supported on PyPy. - Pooling is a separate package,
psycopg_pool(psycopg[pool]), 3.3.3 on 2026-09-22, with sync and async pools.
Repository: 25 open pull requests, 50 merged since 2026-04-05 (source: GitHub search, accessed 2026-10-05). Active.
asyncpg#
Built for asyncio and PostgreSQL only. It implements the PostgreSQL protocol in Cython, uses the
binary format for data, prepares and caches statements automatically, and ships its own connection
pool. It is not DB-API: parameters are $1-style, results are Record objects, and the
interface is fetch, fetchrow, execute. Code written for it does not port to another driver
without edits.
Its FAQ warns that behind pgBouncer in transaction or statement mode the automatic prepared statements fail, and offers three fixes: use asyncpg’s own pool, turn the statement cache off, or change pgBouncer’s mode (source: asyncpg FAQ, accessed 2026-10-05).
Release cadence is the open question. The last tagged release is 0.31.0 (2025-11-24); the one before it was 0.30.0 on 2024-10-20 (source: GitHub releases, accessed 2026-10-05). Meanwhile the main branch is busy: commits on 2026-10-02 and 2026-10-03 add PostgreSQL 19 support, bump uvloop, and fix pool backoff, and 32 pull requests merged since 2026-04-05 against 34 open. Development is healthy; releases are slow.
pg8000#
Pure Python, DB-API, no C dependency. It installs from a single universal wheel, so it runs anywhere Python runs, including environments that cannot load native libraries. It is the slowest driver in the local measurements (S2). Its repository moved from GitHub to Codeberg on 2025-09-14 (source: https://codeberg.org/tlocke/pg8000, accessed 2026-10-05); the last commit there is the same day and 1 pull request is open. Quiet and finished-shaped; see S4.
Cross-cutting facts#
- Wheels. psycopg-binary and psycopg2-binary each publish 66 wheels per release; asyncpg 56; pg8000 and pure psycopg one universal wheel (source: PyPI file lists, accessed 2026-10-05).
- Which driver an ORM uses. SQLAlchemy and Django select psycopg2, psycopg 3, asyncpg or pg8000 through a dialect or backend setting (see 1.180 Python ORMs).
- Pooling. psycopg_pool and asyncpg’s pool live in-process; pgBouncer and similar run outside and constrain prepared statements (see 1.184 Connection Pool Libraries).
S1 Recommendation: Choose on Four Axes#
No driver wins the category, because the databases differ. The choice follows four questions, in this order.
1. Which database, and sync or async?#
| Sync | Async | |
|---|---|---|
| PostgreSQL | psycopg 3 (psycopg2 for existing code) | psycopg 3 async classes, or asyncpg |
| MySQL / MariaDB | mysqlclient (C) or PyMySQL (pure Python) | asyncmy, or aiomysql |
| SQLite | sqlite3 | aiosqlite |
2. Do you need the DB-API?#
asyncpg is the only profiled driver outside PEP 249 families. Choosing it buys the fastest
PostgreSQL path and its own pool, and costs portability: its placeholders and result types are its
own. Everything else keeps a cursor-and-execute shape that frameworks and tools expect.
3. Can the install compile?#
| Environment | Works without a compiler |
|---|---|
| Linux, macOS, Windows with PyPI wheels | psycopg[binary], psycopg2-binary, asyncpg, asyncmy, PyMySQL, pg8000, aiomysql |
| Linux or macOS, plain package name | PyMySQL, pg8000, psycopg (pure Python, needs system libpq) |
| Needs headers and a compiler | psycopg2, mysqlclient, psycopg[c], mariadb |
psycopg2 and mysqlclient are the two common drivers whose primary package has no Linux or macOS wheel.
4. Is the project moving?#
- Healthy by the stall test: psycopg 3, psycopg2, asyncpg, PyMySQL, mysqlclient, asyncmy.
- Stall signal: aiomysql, aiosqlite (many open pull requests, none merged in six months).
- Quiet and finished-shaped: pg8000 (moved to Codeberg 2025-09, 1 open pull request).
(source: GitHub search API, accessed 2026-10-05; table in S2 and S4)
The durable finding#
Where a driver comes from decides more than how fast it is. A pure-Python driver installs
anywhere, a C binding needs the vendor’s library, and the binary-wheel packages trade operating
system patching of libpq and libssl for a one-line install. Pick the install path first, the
async shape second, and read the speed table last.
S1: SQLite Drivers#
SQLite has no server and no wire protocol: the engine is a library linked into the process. A “driver” here is a thin Python binding plus transaction and type handling.
At a glance#
| Driver | Latest | Interface | Implementation | Async | License |
|---|---|---|---|---|---|
sqlite3 | ships with Python | DB-API | C module in the standard library | No | Python license |
| aiosqlite | 0.22.1 (2025-12-23) | DB-API-like, awaitable | Wraps sqlite3, one worker thread per connection | Yes | MIT (repository; PyPI metadata field is empty) |
(source: PyPI JSON API and https://github.com/omnilib/aiosqlite, accessed 2026-10-05)
sqlite3#
Part of CPython, nothing to install, DB-API 2.0 compliant. It is also the reference point for “fast”: a point query returns in microseconds because no socket is involved (S2).
Behavior worth knowing before relying on it:
- Transaction control changed in Python 3.12. Before it, the module opened transactions
implicitly around data-changing statements, governed by
isolation_level. Python 3.12 added anautocommitattribute that gives PEP 249-conformant behavior when set toFalseand leaves the legacy behavior as the default (source: Pythonsqlite3documentation, accessed 2026-10-05). - Thread rule. A connection is bound to its creating thread unless
check_same_thread=Falseis passed, and then the caller owns the locking. - Library version. The SQLite version is whatever the Python build links, so features such as
RETURNINGorSTRICTtables depend on the interpreter, not on the project’s pins.
aiosqlite#
Runs each connection’s sqlite3 calls on a dedicated thread and hands results back to the event
loop through futures. It exists so that SQLite use does not block a coroutine; it does not make
SQLite concurrent. Each awaited call pays a thread hand-off, which dominates the cost of small
queries (S2).
Stall signal. 16 open pull requests, none merged since 2026-04-05, and no commit to the default branch in 2026; the last release is 0.22.1 on 2025-12-23 (source: GitHub search and commits API, accessed 2026-10-05). The 2025 releases were frequent (0.22.0 on 2025-12-13 and 0.22.1 ten days later), so the pause is recent. A wrapper this small is easy to replace, which lowers the cost of the signal.
Cross-cutting facts#
- Concurrency is a SQLite property, not a driver property. One writer at a time, readers alongside in WAL mode, for either driver.
- ORMs. SQLAlchemy offers
sqlite3and aiosqlite dialects; see 1.180 Python ORMs. - Pools. A file-backed SQLite database gains little from client-side pooling; see 1.184.
S2: Comprehensive
S2 Comprehensive Discovery: Methods Note#
Two kinds of evidence appear in S2: facts read from registries and documentation, each with its source, and local measurements of install footprint and database round trips.
Measurement plan#
| Level | What it settles | Cost | Rung | Coverage |
|---|---|---|---|---|
| L0 registry facts | versions, licenses, wheel platforms, stall test | minutes | cited | all drivers |
| L1 install footprint and import time | what each package adds to an environment; whether a plain install builds | minutes | measured-local | all profiled drivers; two source builds fail here |
| L2 round trips against a local server | relative cost per operation across drivers | about 30 minutes | measured-local | 11 driver configurations; mysqlclient excluded (source build fails) |
| deferred | a second CPU architecture, a remote server with real network latency, TLS, large concurrency | days | not run | none |
The cut line: one machine, throwaway containers, no second host.
Environment (all L1 and L2 numbers)#
- aarch64, Linux 6.6.87.2 under WSL2, 8 logical CPUs, Python 3.12.3, packages installed with
uvinto fresh virtual environments (one per configuration). - PostgreSQL 18.6 and MySQL 8.4 in Docker containers, published on loopback ports, data on tmpfs,
PostgreSQL with
fsyncandsynchronous_commitoff. The container port mapping adds a fixed cost to every round trip for every driver alike. - The host was shared with other jobs (load average 2 to 4 on 8 CPUs during runs). A repeat of three configurations moved the point-query median of psycopg 3 by about 30 percent (595 to 781 microseconds), so differences smaller than that are noise.
- Median of 5 repetitions per workload. One interpreter per driver. Table
thas 100,000 rows of an integer key, a 29-character string and a float;t2receives inserts.
Workloads#
| Name | What it does |
|---|---|
| connect | open and close 200 connections, reported per connect |
| point | 5,000 sequential single-row primary-key SELECTs, reported per query |
| fetch100k | SELECT all three columns of all 100,000 rows and fetch them all |
| insert20k | 20,000 rows through the driver’s executemany, one transaction |
| concurrent | async drivers only: 10 connections each running 500 point queries at once |
Placeholders use each driver’s own style. No prepared-statement tuning, no pool, no pipeline mode switched on by hand: defaults only.
Files#
harness/1-180-1-python-database-drivers/: bench.py, run.py, footprint.py, stall_test.py,
pypi_facts.py, wheels.py, servers.sh, summarize.py and the raw results in results/.
S2: The DB-API Contract and Where Drivers Differ#
What PEP 249 fixes and what it leaves open#
PEP 249 (https://peps.python.org/pep-0249/) fixes the shape: a module-level connect(), connection
objects with cursor(), commit(), rollback() and close(), cursor objects with execute,
executemany, fetchone, fetchmany, fetchall and description, and a standard exception tree
(Error, DatabaseError, IntegrityError and so on). Three module constants describe the driver:
| Constant | Meaning | Values seen |
|---|---|---|
apilevel | DB-API version | “2.0” |
threadsafety | 0 none, 1 threads may share the module, 2 may share connections, 3 may share cursors | varies by driver |
paramstyle | placeholder syntax: qmark, numeric, named, format, pyformat | ? for sqlite3; %s (format or pyformat) for psycopg, pg8000, PyMySQL, mysqlclient |
The spec leaves the following to each driver, and they are where code stops being portable:
- Transaction start. The DB-API says a transaction begins implicitly and
commit()ends it. Drivers differ on what “autocommit” means and what its default is (psycopg 3 and psycopg2 start a transaction on the first statement;sqlite3opened transactions only around data-changing statements until Python 3.12 added an opt-inautocommitattribute). - Placeholders.
%sagainst?against$1. A query written for one driver needs editing for another; ORMs and query builders hide this. - Type adaptation. What a
numericcolumn returns (Decimal), what a timestamp with a time zone returns, how arrays and JSON map. Each driver ships its own adapter tables. - Async. PEP 249 has nothing on coroutines.
The same lookup, written for each style, is the whole portability problem in four lines:
sqlite3 cur.execute("SELECT val FROM t WHERE id = ?", (7,))
psycopg, MySQL cur.execute("SELECT val FROM t WHERE id = %s", (7,))
asyncpg await conn.fetchrow("SELECT val FROM t WHERE id = $1", 7)Parameter handling: client-side or server-side#
| Driver | Where parameters are bound |
|---|---|
| psycopg2 | Client: the driver builds the final SQL string and sends it as a simple query |
| psycopg 3 | Server: query text and parameters sent separately through the extended protocol |
| asyncpg | Server: always the extended protocol with a prepared statement |
| pg8000 | Server: extended protocol |
| PyMySQL, mysqlclient, aiomysql, asyncmy | Client: escaped and interpolated into the SQL text by default |
sqlite3 | In process: bound by the engine |
(source for psycopg and asyncpg: psycopg 3 documentation “Differences from psycopg2” and asyncpg documentation, accessed 2026-10-05; other rows from each driver’s documented behavior, not re-tested here)
Server-side binding closes off a class of quoting mistakes and enables plan reuse. It also changes
what runs: psycopg 3 will not run several statements in one execute when parameters are passed,
and does not accept parameters in SET or NOTIFY, both documented migration breaks from psycopg2.
Prepared statements, poolers and the asyncpg trap#
asyncpg prepares and caches statements automatically. Behind pgBouncer in transaction or statement pooling mode this fails, because the prepared statement lives on a server connection that the next transaction may not get. The asyncpg FAQ lists three remedies: use asyncpg’s own pool, disable its statement cache, or run pgBouncer in session mode (source: asyncpg FAQ, accessed 2026-10-05). psycopg 3 has the same class of issue through its automatic preparation, and exposes a setting to turn it off. The pooling survey (1.184 Connection Pool Libraries) covers the pooler side.
Pools#
| Driver | In-process pool |
|---|---|
| psycopg 3 | psycopg_pool (separate package, psycopg[pool], 3.3.3), sync and async |
| psycopg2 | psycopg2.pool classes in the main package: simple, threaded |
| asyncpg | Built in (create_pool) |
| aiomysql | Built in |
| asyncmy | Built in |
pg8000, PyMySQL, mysqlclient, sqlite3 | None; use an application or ORM-level pool |
SQLAlchemy supplies its own pool above any driver. A driver pool and an ORM pool stacked together count connections twice, so pick one layer to own the limit.
Async interface shapes#
| Driver | Shape |
|---|---|
| psycopg 3 | AsyncConnection and AsyncCursor, same method names as sync with await |
| asyncpg | Own interface: fetch, fetchrow, fetchval, execute, executemany, copy_* |
| aiomysql, asyncmy | Cursor interface with await |
| aiosqlite | Cursor interface with await, thread-backed |
Code that must run on both sync and async stacks is easiest with psycopg 3, whose two interfaces mirror each other.
S2: Install Path, Wheels and Footprint#
Which platforms get a binary wheel#
Counted from the file list of each package’s latest release (source: PyPI JSON API, accessed 2026-10-05).
| Package | Release | Wheels | Linux | macOS | Windows | Source dist |
|---|---|---|---|---|---|---|
| psycopg2 | 2.9.13 | 6 | none | none | 6 | yes |
| psycopg2-binary | 2.9.13 | 66 | 6 aarch64, 6 x86_64, 24 musl | 12 | 6 | yes |
| psycopg | 3.3.6 | 1 universal | any | any | any | yes |
| psycopg-binary | 3.3.6 | 66 | 6 aarch64, 6 x86_64, 24 musl | 12 | 6 | no |
| psycopg-c | 3.3.6 | 0 | build only | build only | build only | yes |
| asyncpg | 0.31.0 | 56 | 7 aarch64, 7 x86_64, 14 musl | 14 | 14 | yes |
| pg8000 | 1.31.5 | 1 universal | any | any | any | yes |
| mysqlclient | 2.3.0 | 16 | none | none | 16 | yes |
| PyMySQL | 1.2.3 | 1 universal | any | any | any | yes |
| aiomysql | 0.3.2 | 1 universal | any | any | any | yes |
| asyncmy | 0.2.15 | 64 | 8 aarch64, 8 x86_64, 16 musl | 16 | 16 | yes |
| aiosqlite | 0.22.1 | 1 universal | any | any | any | yes |
| mariadb | 1.1.14 | 12 | none | none | 12 | yes |
| mysql-connector-python | 26.7.0 | 25 | 5 aarch64, 5 x86_64 | 10 | 4 | yes |
The pattern: the packages named for a driver that wraps a vendor C library (psycopg2, mysqlclient, mariadb, psycopg-c) ship no Linux or macOS wheel under that name. The wheel-bearing versions are either a differently named package (psycopg2-binary, psycopg-binary) that bundles the C library, or a driver that does not wrap one at all (asyncpg, asyncmy, PyMySQL, pg8000).
What a source build needs#
Tried in a fresh Python 3.12 environment on a machine with gcc 13.3, the runtime libpq.so.5 and
libmysqlclient.so.21, and no development headers (measured 2026-10-05; uv; aarch64):
psycopg2from source failed at the C compile step;pg_configreports an include directory that does not containlibpq-fe.h.mysqlclientfrom source failed at configuration with “Can not find valid pkg-config name” and the instruction to setMYSQLCLIENT_CFLAGSandMYSQLCLIENT_LDFLAGS.
Having the runtime library installed is not enough. The headers come from separate development packages on Linux distributions, and from a client-library install elsewhere. In a slim container image this is the usual first failure.
The binary-wheel trade-off#
The psycopg2 documentation states that psycopg2-binary “is meant for beginners”, that library
maintainers should not depend on it, and that “for production use you are advised to use the source
distribution” (source: https://www.psycopg.org/docs/install.html, accessed 2026-10-05). Its two
concrete reasons: the wheel carries its own libpq and libssl, so operating system security
updates do not reach them, and the bundled libssl can conflict with other modules that load
OpenSSL, including Python’s ssl module, with a possible segfault under concurrency in some cases.
psycopg 3 inherits the shape (psycopg[binary] bundles libpq, built “on a best-effort basis”,
absent on PyPy and on macOS releases older than 14 for ARM64 after 3.1.20) but its documentation
recommends the binary route “for most users” and presents the source-built C accelerator
(psycopg[c]) as the alternative when system libraries must be shared (source:
https://www.psycopg.org/psycopg3/docs/basic/install.html, accessed 2026-10-05). The bundled-library risk is the same in both; the
two documentation sets weigh it differently.
Decision rule: a service whose container image is rebuilt on every base-image security update
loses little by bundling libpq; a long-lived host that relies on the distribution to patch
libssl is better served by the source build or by a pure-Python driver.
Footprint and import time#
Added disk space is site-packages minus an empty environment (10,100 bytes, effectively zero);
import time is the median of 15 fresh interpreters minus the median of python -c pass
(11.7 ms). (measured 2026-10-05; Python 3.12.3, aarch64 WSL2; uv)
| Configuration | Added disk (KB) | Distributions | Import (ms) |
|---|---|---|---|
sqlite3 (stdlib) | 0 | 0 | 6.2 |
| aiosqlite | 55 | 1 | 39.1 |
| PyMySQL | 144 | 1 | 25.1 |
| aiomysql (and PyMySQL) | 358 | 2 | 51.3 |
| psycopg (pure Python) | 904 | 2 | 135.1 |
| pg8000 | 1,227 | 5 | 82.7 |
| asyncpg | 13,040 | 1 | 65.5 |
| psycopg2-binary | 16,347 | 1 | 16.8 |
| psycopg[binary] | 25,411 | 3 | 132.3 |
| psycopg[binary,pool] | 25,537 | 4 | 152.0 |
| asyncmy | 32,857 | 1 | 86.3 |
| mysql-connector-python | 66,770 | 1 | 69.3 |
Reading it:
- psycopg 3 imports in about eight times the time of psycopg2 (132.3 against 16.8 ms) in this
measurement, whether or not the C accelerator is present: the pure-Python configuration takes
135.1 ms too, so the cost sits in the package and not in the extension.
-X importtimeput thepsycopg.pqsubpackage at about 70 of 109 ms cumulative (measured 2026-10-05; psycopg 3.3.6). A repeat of the plain import measured higher than the first run (about 175 ms net), so treat these as ranges on a shared host. Relevant to command-line tools and serverless cold starts, not to long-running services. - Async drivers include the cost of importing
asyncio, about 35 ms net on this machine (measured 2026-10-05;import asyncio, 49.0 ms against 14.2 ms forpass), which is most of aiosqlite’s 39.1 ms against a 6.2 ms stdlib module. - Bundled client libraries dominate disk size: psycopg-binary adds about 25 MB, psycopg2-binary about 16 MB; asyncmy’s 33 MB is its compiled extensions. Neither matters on a server and both matter in a size-limited function bundle.
S2 Recommendation: Five Findings#
Install path separates the drivers before speed does. psycopg2, mysqlclient, mariadb and psycopg-c ship no Linux or macOS wheel under their own names (PyPI file lists, accessed 2026-10-05), and a source build failed here on the missing development headers for both psycopg2 and mysqlclient (measured 2026-10-05; uv; aarch64). The wheel-bearing options are the
-binarypackages, which bundlelibpqandlibssl, and the drivers with no C-library dependency.psycopg2-binary’s own documentation advises against it for production, citing bundled libraries that miss operating system updates and an OpenSSL conflict that can segfault under concurrency (https://www.psycopg.org/docs/install.html). psycopg 3’s documentation recommends
psycopg[binary]for most users and keeps the source-builtpsycopg[c]as the shared-library alternative.For small queries over a network the driver is not the bottleneck. Single-row query medians across psycopg, asyncpg, PyMySQL, aiomysql and asyncmy sat between 529 and 934 us and overlapped within the measured noise; only pure-Python pg8000 stood out (1,608 us). The compiled drivers win on volume: 100,000-row fetches ran four to nine times faster in psycopg’s binary mode than in its pure-Python mode, and about four times faster in asyncmy than in PyMySQL.
executemanyis a round-trip question. psycopg2’s plainexecutemanytook 11.2 s for 20,000 rows, psycopg 3’s 0.21 s, asyncpg’s 0.16 s (measured 2026-10-05; PostgreSQL 18.6 on loopback). Moving a bulk-writing service from psycopg2 to psycopg 3 or asyncpg removes most of the cost without a code change beyond the import. psycopg 3’s asyncexecutemanywas an unexplained exception at 1.1 s.Two async wrappers show stall signals. aiomysql (35 open pull requests, none merged since 2026-04-05) and aiosqlite (16 open, none merged) have no commits to their default branch in 2026 (GitHub, accessed 2026-10-05). Both still work. asyncpg releases slowly (0.31.0 on 2025-11-24, previous 0.30.0 on 2024-10-20) while its main branch is active.
Defaults by situation#
| Situation | Default |
|---|---|
| PostgreSQL, sync or mixed sync/async, new code | psycopg 3 with psycopg[binary] (source-built psycopg[c] where system libraries must be shared) |
| PostgreSQL, async only, throughput matters, no DB-API need | asyncpg |
| PostgreSQL, no native code allowed | pg8000 (slow) or psycopg pure Python |
| PostgreSQL, existing psycopg2 code | stay on psycopg2 built from source; migrate when bulk writes or async matter |
| MySQL or MariaDB, sync | PyMySQL for zero-install, mysqlclient where the headers are managed |
| MySQL or MariaDB, async | asyncmy; aiomysql as the established alternative with the stall signal |
| SQLite | sqlite3; aiosqlite only to keep an event loop free |
S2: Round-Trip Measurements#
Method, environment and noise bounds are in approach.md. All figures below are medians of 5
repetitions (measured 2026-10-05; Python 3.12.3; aarch64 WSL2; PostgreSQL 18.6 and MySQL 8.4 in
containers on loopback; default driver settings). Differences under about 30 percent are inside the
measured noise on this shared host.
PostgreSQL#
| Configuration | Version | connect (ms) | point query (us) | fetch 100k rows (ms) | insert 20k rows (ms) | 10x500 concurrent (ms) |
|---|---|---|---|---|---|---|
| psycopg2-binary | 2.9.13 | 10.6 | 529 | 93 | 11,155 | n/a |
| psycopg[binary] sync | 3.3.6 | 12.5 | 595 | 126 | 214 | n/a |
| psycopg pure Python sync | 3.3.6 | 10.2 | 598 | 556 | 424 | n/a |
| psycopg[binary] async | 3.3.6 | 10.8 | 807 | 63 | 1,126 | 433 |
| asyncpg | 0.31.0 | 32.5 | 750 | 95 | 159 | 399 |
| pg8000 | 1.31.5 | 29.3 | 1,608 | 252 | 37,561 | n/a |
A second run of three configurations (psycopg sync, psycopg async, asyncpg) gave: point queries 781, 934 and 837 us; 100k fetch 86, 82 and 61 ms; 20k insert 210, 1,098 and 166 ms; concurrent 412 and 403 ms for psycopg async and asyncpg.
MySQL (8.4)#
| Configuration | Version | connect (ms) | point query (us) | fetch 100k rows (ms) | insert 20k rows (ms) | 10x500 concurrent (ms) |
|---|---|---|---|---|---|---|
| PyMySQL | 1.2.3 | 29.7 | 621 | 285 | 304 | n/a |
| aiomysql | 0.3.2 | 2.8 | 671 | 381 | 258 | 432 |
| asyncmy | 0.2.15 | 3.3 | 807 | 68 | 237 | 413 |
mysqlclient is absent from the table: its source build fails on this machine (see
install-and-wheels.md), and a driver that cannot be installed without extra system packages was
not forced into the comparison.
SQLite (file on tmpfs)#
| Configuration | connect (ms) | point query (us) | fetch 100k rows (ms) | insert 20k rows (ms) | 10x500 concurrent (ms) |
|---|---|---|---|---|---|
sqlite3 | 0.021 | 6.6 | 77 | 10.9 | n/a |
| aiosqlite 0.22.1 | 0.70 | 239 | 96 | 16.8 | 2,907 |
What the numbers say#
Single-row queries are a tie among the C and Cython drivers on a networked database. The psycopg, asyncpg, PyMySQL, aiomysql and asyncmy point-query medians fall between 529 and 934 us, a spread that overlaps the repeat-run drift. The cost is the network hop, here a loopback port mapping, and not the driver. pg8000, pure Python, took about 1.6 ms, two to three times the others. Against a remote server, with a real round trip in the hundreds of microseconds to milliseconds, driver differences on small queries shrink further.
Large result sets separate the implementations. Fetching 100,000 rows took 63 to 126 ms for psycopg binary and asyncpg, and 556 ms for the same psycopg package in pure-Python mode: about four to nine times the binary run (556 against 126 and 63 ms). PyMySQL took 285 ms against asyncmy’s 68 ms, about four times. aiomysql, which reuses PyMySQL’s parsing, took 381 ms. This is the case for choosing the compiled option when the workload returns many rows per query.
executemany is the largest difference in the tables, and it is about round trips.
psycopg2’s executemany took 11.2 s for 20,000 rows, about 0.56 ms per row, the same as one point
query: one network round trip per row. pg8000 took 37.6 s, about 1.9 ms per row. psycopg 3 took
0.21 s for the same rows, 50 times faster than psycopg2 (11,155 against 214 ms), consistent with
pipelining several statements per round trip; asyncpg took 0.16 s. psycopg2 offers batch helpers
in its extras module (execute_batch, execute_values) that close most of the gap, but the plain
DB-API method does not. The MySQL drivers finished 20,000 rows in about 0.24 to 0.30 s, which fits
PyMySQL-family executemany collapsing an INSERT ... VALUES into one multi-row statement
(not verified beyond timing).
One anomaly, reproduced and unexplained. psycopg 3’s async executemany took 1.1 s for the
20,000 rows in both runs, five times its own sync figure (214 and 210 ms) on the same server. The
cause was not investigated. Bulk loads through the async interface are worth timing before
adopting it; COPY is the PostgreSQL-native bulk path and was not measured.
Concurrency on async drivers is equal at this scale. Ten connections each running 500 point queries finished in 399 to 433 ms for asyncpg, psycopg async, aiomysql and asyncmy: 5,000 queries in total, or about 11,500 to 12,500 queries per second, eight to nine times the single-connection rate implied by the point-query column. The drivers are equal here; which has the lower overhead at hundreds of connections was not tested.
aiosqlite pays a thread hop per call. A point query took 239 us against 6.6 us for sqlite3,
about 36 times as long, with connect at 0.70 ms against 0.021 ms. Large operations amortize it
(fetch 96 against 77 ms, insert 16.8 against 10.9 ms). The concurrent test, ten connections each
with its own worker thread, took 2.9 s for 5,000 queries, slower than one sqlite3 connection
would take for all of them (about 33 ms at 6.6 us each, extrapolated). What aiosqlite buys is a
free event loop during slow queries, not speed.
Connect times are not comparable across the tables. They range from 0.02 ms (SQLite) to 33 ms, and the PyMySQL and aiomysql connects differ by a factor of 10 (29.7 and 2.8 ms) despite sharing protocol code. Authentication handshakes differ by server and driver, and the pattern was not traced. The practical reading is that connecting costs milliseconds on PostgreSQL and MySQL and that a pool (1.184 Connection Pool Libraries) removes it.
Not measured#
Remote servers and TLS, prepared-statement effects behind pgBouncer, COPY, large concurrency
beyond ten connections, pool implementations (psycopg_pool, asyncpg’s pool, aiomysql’s pool),
x86-64, and mysqlclient.
S3: Need-Driven
S3 Need-Driven Discovery: Personas#
| Persona | Situation | What eliminates options | What remains |
|---|---|---|---|
| Web app migrating from psycopg2 to psycopg 3 | Sync framework app, PostgreSQL, existing SQL | Anything not DB-API compatible (asyncpg) | psycopg 3 binary or source-built |
| High-throughput async service | asyncio, PostgreSQL, many concurrent requests | Sync drivers, pg8000 | asyncpg, psycopg 3 async |
| Data script on SQLite | One file, one process, pandas or plain SQL | Anything with a server or a compile step | sqlite3 |
| MySQL shop | MySQL or MariaDB, mixed sync and async code | Drivers needing system headers where the base image is slim | PyMySQL, asyncmy, mysqlclient |
| Locked-down or minimal image | No compiler, read-only filesystem, or a size cap | Source builds, large bundled wheels | pure-Python drivers, pg8000, PyMySQL |
Files: use-case-psycopg2-migration.md, use-case-async-service.md,
use-case-sqlite-data-script.md, use-case-mysql-shop.md, use-case-minimal-image.md.
S3 Recommendation: By Persona#
| Persona | Pick | Avoid |
|---|---|---|
| Web app migrating from psycopg2 | psycopg 3 (psycopg[binary] in rebuilt containers, psycopg[c] on shared hosts) | asyncpg for a migration that must stay DB-API |
| High-throughput async service | asyncpg for PostgreSQL-only and speed; psycopg 3 async plus psycopg_pool for DB-API or mixed sync/async | A pooler in transaction mode with asyncpg’s default statement cache |
| Data script on SQLite | sqlite3 | aiosqlite unless the script is already async |
| MySQL shop | asyncmy (async), PyMySQL (sync, zero install), mysqlclient where headers are managed | mysqlclient on slim images without a headers step; aiomysql for new work while its stall signal stands |
| Minimal or locked-down image | PyMySQL, pg8000, sqlite3; psycopg pure Python if libpq is present | Anything that builds from source; large bundled wheels under a size cap |
Common thread: the decision turns on install path and interface shape, and speed matters only for large result sets and bulk writes.
Persona: high-throughput async service#
Situation#
An asyncio service (an API gateway, a worker pulling jobs) on PostgreSQL, with many concurrent requests and short queries. A fixed pool of connections is shared across requests. Performance and tail latency matter; the team controls the SQL.
What it eliminates#
- Sync drivers on the event loop: a blocking call stalls every request.
- pg8000 and aiosqlite-style thread-hop designs for hot paths.
What remains#
| Option | Fits when |
|---|---|
| asyncpg | The service is PostgreSQL-only, the team accepts a non-DB-API interface, and it wants the built-in pool and the fastest binary-protocol path |
psycopg 3 async with psycopg_pool | The code base also has sync paths, or needs DB-API shape, COPY through the standard interface, or ORM support on both stacks |
In the local measurements the two were indistinguishable on the concurrency test: ten connections
each issuing 500 point queries finished in 403 to 433 ms on both (measured 2026-10-05; S2). Both
fetched 100,000 rows in 60 to 95 ms. The difference that appeared was bulk insert through
executemany: asyncpg 0.16 s, psycopg async 1.1 s in both runs, unexplained. A service that bulk
loads through the async path should time its own case.
The pooler trap#
asyncpg’s automatic prepared statements break behind pgBouncer in transaction or statement mode. Three remedies exist (own pool, no statement cache, session mode); choose one before deployment (S2; 1.184 Connection Pool Libraries). psycopg 3’s automatic preparation has the same exposure and a switch to disable it.
Constraints and trade-offs#
- asyncpg releases slowly (0.31.0 on 2025-11-24, 0.30.0 on 2024-10-20), so support for a new Python or PostgreSQL version can wait on the next tag even when main already has it.
- psycopg 3’s binary wheels cover Linux x86-64 and aarch64, glibc and musl; asyncpg’s too.
Decision criteria#
DB-API shape or sync reuse: psycopg 3. PostgreSQL-only, raw speed, own interface acceptable: asyncpg. For either, decide who owns the connection limit: the driver pool, an ORM pool or an external pooler, not two of them.
Persona: locked-down or minimal image#
Situation#
A deployment where native code is a problem: a distroless or scratch-style container with no compiler, a serverless bundle with a size limit, a platform that forbids loading unsigned shared libraries, or a build farm on a CPU architecture few wheels target.
What it eliminates#
- Source builds (psycopg2, mysqlclient, psycopg[c], mariadb).
- Large bundled wheels where a size cap applies: psycopg[binary] added about 25 MB, asyncmy about 33 MB, mysql-connector-python about 67 MB (measured 2026-10-05; S2).
What remains#
| Database | Driver | Disk added | Import (net) |
|---|---|---|---|
| PostgreSQL | pg8000 | 1.2 MB, 5 packages | 83 ms |
| PostgreSQL | psycopg pure Python | 0.9 MB, needs system libpq | 135 ms |
| MySQL | PyMySQL | 144 KB | 25 ms |
| MySQL, async | aiomysql | 358 KB, 2 packages | 51 ms |
| SQLite | sqlite3 | none | 6 ms |
| SQLite, async | aiosqlite | 55 KB | 39 ms |
(measured 2026-10-05; Python 3.12.3; aarch64; uv)
Trade-offs#
Pure-Python PostgreSQL drivers cost speed: pg8000 took 1.6 ms per point query and 252 ms for
100,000 rows, and its executemany took 37.6 s for 20,000 rows over loopback (S2). psycopg in
pure-Python mode still needs the system libpq shared library, which a distroless image may not
have, so check ldconfig or the image’s package list before choosing it.
Decision criteria#
If the image can carry one shared library, psycopg pure Python with libpq is the faster
pure-Python route for PostgreSQL. If it cannot, pg8000 is the only option in this survey that
needs nothing, and bulk loads should go through multi-row statements or COPY where supported
rather than executemany.
Persona: MySQL shop#
Situation#
An organization standardized on MySQL or MariaDB. Services are a mix: a few older synchronous applications, newer asyncio services, internal tools. Images are slim, builds are automated, and a licensing review has opinions.
What it eliminates#
- Drivers that compile against system headers, unless the base images provide them: mysqlclient 2.3.0 ships Windows wheels only, and its source build failed here with no development headers (S2). mariadb has the same shape.
- GPL-2.0-or-later components, where redistribution of a closed product is planned: mysqlclient and mysql-connector-python (GPL-2.0 with a FOSS exception) need review. PyMySQL, aiomysql (MIT), and asyncmy (Apache-2.0) do not.
What remains#
| Need | Pick | Notes |
|---|---|---|
| Sync, installs anywhere | PyMySQL | Pure Python; MIT; 35 pull requests merged since 2026-04-05. About four times slower than the C and Cython drivers on a 100,000-row fetch (285 against 68 ms, S2) |
| Sync, speed, managed headers | mysqlclient | C binding; the base image must provide development packages |
| Async | asyncmy | Cython, 64 wheels, Apache-2.0; fastest large fetch measured (68 ms) |
| Async, established | aiomysql | MIT; stall signal: 35 open pull requests, none merged since 2026-04-05, no commits to the default branch in 2026 |
MySQL 8 authentication#
The default caching_sha2_password plugin needs TLS or RSA password exchange; PyMySQL-family
drivers need the cryptography package for it (installed explicitly for the aiomysql run in S2).
An authentication error right after a server upgrade usually points here.
Decision criteria#
Prefer the asyncmy and PyMySQL pair for new work: both are active, permissively licensed and install without headers. Keep mysqlclient where it is already deployed and the headers are in the base image. Watch aiomysql: it works, and its maintainers are not merging.
MariaDB-specific features (its own connector, mariadb) are a separate decision: that package is
LGPL-2.1, has Windows wheels only, and needs MariaDB Connector/C elsewhere.
Persona: web app migrating from psycopg2 to psycopg 3#
Situation#
A synchronous web application, a few years old, PostgreSQL, SQL written against psycopg2
(%s placeholders, psycopg2.extras, possibly execute_values). Deployed in containers. The team
wants a maintained driver with async available later, and does not want a rewrite.
What it eliminates#
- asyncpg: different placeholders and result types; a migration to it is a rewrite of every query call.
- pg8000: DB-API compatible but slower in every measured workload (S2).
What remains: psycopg 3#
It keeps the cursor interface and %s placeholders, so most query code moves by changing the
import and the connection call. The differences that do bite are documented migration breaks:
- Parameters are bound on the server. Statements that relied on client-side interpolation fail:
several statements in one
executewith parameters, and parameters inSETandNOTIFY(source: psycopg 3 docs, “Differences from psycopg2”, accessed 2026-10-05). - The
psycopg2.extrashelpers have different equivalents (row factories,executemanywith pipelining,COPYas an iterator). - Pooling moves to
psycopg_pool, a separate package. - Import time is higher, about 132 ms against 17 ms net (S2); irrelevant to a long-running server.
Install decision#
| Deployment | Choice |
|---|---|
| Container rebuilt on base-image updates | psycopg[binary]: one line, and the bundled libraries refresh with the rebuild |
| Long-lived host patched by the distribution | psycopg[c] built from source against the system libpq, or pure Python with the system library |
The binary package is the documented equivalent of psycopg2-binary, and carries the same bundled-library caveat (S2).
What the move gains#
Bulk writes: 20,000 rows through executemany took 0.21 s on psycopg 3 against 11.2 s on psycopg2
over loopback (measured 2026-10-05; S2). Single-row queries were a tie, so a read-heavy app gains
little in latency and gains a maintained driver with native async.
Decision criteria#
- Heavy
psycopg2.extrasuse, many multi-statement strings, or code that formats SQL by hand: budget real migration time, and run the test suite against server-side binding first. - Mostly ORM-driven: change the dialect setting (see 1.180 Python ORMs) and run the tests.
Persona: data script on SQLite#
Situation#
A script or notebook that loads files into a local SQLite database, queries it and writes results. One process, one file, run by hand or from a scheduler. The author wants nothing to install.
What it eliminates#
Everything with a server, a wire protocol or a compile step. SQLite is a library in the process.
What remains: sqlite3#
It ships with Python, follows DB-API, and is the fastest option measured: 6.6 us per point query and 10.9 ms for 20,000 inserts in one transaction (measured 2026-10-05; Python 3.12.3; file on tmpfs).
Points that matter for scripts:
- Wrap bulk inserts in one transaction. The 20,000-row figure assumes it; per-row commits cost a disk sync each on a real disk.
- Python 3.12’s
autocommitattribute gives PEP 249 behavior when set explicitly; the default keeps the legacy implicit-transaction behavior (source: Pythonsqlite3docs, accessed 2026-10-05). - The SQLite version is the interpreter’s. Check
sqlite3.sqlite_versionif a script needs a recent SQL feature. - Threads. A connection belongs to its creating thread by default.
When aiosqlite enters#
Only when the script is already an asyncio program and a slow query must not block other tasks. It costs a thread hop per call: 239 us per point query against 6.6 us (about 36 times) and 2.9 s for ten concurrent connections doing 5,000 point queries in total (S2). It is MIT licensed and shows a stall signal (16 open pull requests, none merged since 2026-04-05), though the wrapper is small enough to replace.
Constraints#
SQLite allows one writer at a time. Several processes writing the same file need WAL mode and tolerance for lock waits; a workload that outgrows that is the point to move to a server database and a driver from the PostgreSQL or MySQL rows of this survey.
S4: Strategic
S4 Strategic Discovery: Viability Signals#
Drivers are low in the stack and hard to replace once SQL, pooling and error handling are written against them, so durability matters more here than in most categories. Signals used:
- Stall test. Open pull requests against pull requests merged since 2026-04-05, from the GitHub search API on 2026-10-05, read as four cells: arriving and merged is healthy, arriving and not merged is a stall, nothing arriving is finished or forgotten.
- Release cadence, from PyPI.
- Python-version floor. Latest releases require Python 3.10 or newer for psycopg2, psycopg,
mysqlclient and mysql-connector-python, and 3.9 or newer for the rest (PyPI
requires_python, accessed 2026-10-05). - Switching cost: how much code is written against the driver’s own interface.
- Governance. Who owns the repository.
The stall test is a durability signal. It says nothing about whether the code works.
S4 Recommendation: Keep the Driver Replaceable#
Strategic stances#
| Stance | When | Choice |
|---|---|---|
| Conservative | Long-lived service, small team | psycopg2 built from source, or psycopg 3 binary in rebuilt containers; PyMySQL; sqlite3 |
| Performance-first | Async, high volume, PostgreSQL | asyncpg behind a thin data-access layer; psycopg 3 async if DB-API shape is needed |
| Adaptive | Unsure which database will win | An ORM or query layer (1.180) above psycopg 3 or PyMySQL, so the dialect line is the only change |
Switch triggers#
- Leave psycopg2-binary when the service is long-lived and relies on the operating system to
patch
libssl, or when a segfault under concurrency appears with another OpenSSL-linked module. - Move from psycopg2 to psycopg 3 when bulk writes or async paths are needed (S2 measured
11.2 s against 0.21 s for 20,000 rows through
executemany). - Leave aiomysql for asyncmy when the next Python release needs a fix upstream does not merge.
- Leave aiosqlite for a thread-pool around
sqlite3under the same condition. - Leave asyncpg if a required PostgreSQL or Python version waits on a tag.
- Revisit pg8000 if its single maintainer goes silent after the Codeberg move.
Re-measure when#
A new major version of psycopg, asyncpg or asyncmy ships; on an x86-64 host before quoting these speed figures there; and against a remote server with TLS before sizing a pool.
S4: MySQL and SQLite Driver Viability#
Stall-test figures are in viability.md.
PyMySQL and mysqlclient: both under one organization, both merging steadily. mysqlclient’s GPL-2.0-or-later license and Windows-only wheels are the standing cost; PyMySQL’s pure-Python speed is the standing cost on the other side.
asyncmy: healthy, with a much smaller audience than its peers: 391 stars against asyncpg’s 8,098 (GitHub, accessed 2026-10-05). Wrap it behind a thin interface.
aiomysql, aiosqlite: see the table. Both are small; plan a replacement path (asyncmy, or a thread-pool around the sync driver) rather than depending on upstream movement.
S4: PostgreSQL Driver Viability#
Stall-test figures are in viability.md.
psycopg 3: the most active repository in the set and the destination for psycopg2 users.
LGPL-3.0, which concerns only those who modify and redistribute the library itself. Risk: the
three-way packaging (pure, c, binary) multiplies installation paths, and binary wheels are
“best-effort” per platform, so a new Python release can arrive before its wheel.
psycopg2: still released (2.9.13 on 2026-09-09) and merging a few patches. A reasonable place
to remain for years, with an upgrade path to psycopg 3 that is documented and mostly mechanical for
ORM users. Prefer source builds over psycopg2-binary for anything long-lived.
asyncpg: active main branch (PostgreSQL 19 support landed 2026-10-02) and rare releases. The risk is lag between a Python or server release and a tag. The interface is its own, so switching away means rewriting data access; encapsulate it behind a thin layer from the start.
pg8000: no inbound patches and one open pull request; the author moved the project to Codeberg in 2025-09. Pure Python and stable by design, so it fits the “finished” cell; the risk is bus factor and a single-host repository that fewer downstream tools watch.
S4: Viability by Driver#
Stall test, 2026-10-05#
Merged since 2026-04-05 (source: GitHub search API, type:pr, accessed 2026-10-05; pg8000 from the
Codeberg API on the same date).
| Repository | Open PRs | Merged since 2026-04-05 | Cell |
|---|---|---|---|
| psycopg/psycopg | 25 | 50 | healthy |
| psycopg/psycopg2 | 6 | 6 | healthy, low volume |
| MagicStack/asyncpg | 34 | 32 | healthy |
| PyMySQL/PyMySQL | 4 | 35 | healthy |
| PyMySQL/mysqlclient | 1 | 19 | healthy |
| long2ice/asyncmy | 0 | 8 | healthy, nothing waiting |
| mariadb-corporation/mariadb-connector-python | 2 | 5 | healthy |
| aio-libs/aiomysql | 35 | 0 | stalled |
| omnilib/aiosqlite | 16 | 0 | stalled |
| tlocke/pg8000 (Codeberg) | 1 | none found; last commit 2025-09-14 | finished or forgotten |
aiomysql and aiosqlite also have no commit to the default branch in 2026 (commits API, accessed 2026-10-05; aiomysql’s last commits are dependency-bot updates on 2025-12-22, aiosqlite’s last is the 0.22.1 version bump on 2025-12-23). They are Apache- and MIT-licensed code with open patches waiting, which makes a fork or a replacement plausible rather than alarming.
Per-driver notes: viability-postgresql.md and viability-mysql-and-sqlite.md.
Ecosystem direction#
The ORM layer already reaches most of these drivers through dialect settings (1.180 Python ORMs), and SQLAlchemy’s async mode runs on asyncpg, psycopg 3 async, aiomysql, asyncmy and aiosqlite (framing: SQLAlchemy dialect documentation, not re-verified here). That makes the driver the replaceable part of a stack that uses an ORM and the sticky part of a stack that does not.