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#

LibraryTypeVerdictLatest release
psycopg 3DB-API, C accelerator or pure Python, libpqDefault for new PostgreSQL work; sync, async and pool (psycopg_pool)3.3.6 · 2026-09-18
psycopg2DB-API, C binding to libpqMaintained; source build advised for production; plain executemany is one round trip per row2.9.13 · 2026-09-09
asyncpgOwn API, Cython, own protocol implementationFastest async PostgreSQL path; not DB-API; releases slow, main active0.31.0 · 2025-11-24
pg8000DB-API, pure PythonInstalls anywhere; slowest measured; moved to Codeberg 2025-091.31.5 · 2025-09-14
mysqlclientDB-API, C binding to libmysqlclientWindows wheels only; source build needs headers; GPL-2.0-or-later2.3.0 · 2026-09-14
PyMySQLDB-API, pure PythonZero-install MySQL driver; about 4x slower than C on large fetches1.2.3 · 2026-09-17
aiomysqlAwaitable cursor interface over PyMySQL protocol codeStall signal: 35 open PRs, none merged since 2026-04-050.3.2 · 2025-10-22
asyncmyAwaitable cursor interface, CythonActive async MySQL driver with 64 wheels; fastest large fetch measured0.2.15 · 2026-09-21
sqlite3DB-API, C module in the standard libraryDefault for SQLite; 6.6 microseconds per point query measured—
aiosqliteAwaitable cursor interface, thread-backed wrapper of sqlite3Stall signal: 16 open PRs, none merged; about 36x slower per point query than sqlite30.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.

PersonaPick
web app migrating psycopg2psycopg 3; expect server-side binding breaks; bulk writes gain the most
high throughput async serviceasyncpg for PostgreSQL-only speed; psycopg 3 async plus psycopg_pool for DB-API or mixed sync/async; decide pooler interaction first
sqlite data scriptsqlite3; aiosqlite only inside an existing asyncio program
mysql shopasyncmy and PyMySQL; mysqlclient where headers are managed; watch aiomysql
minimal imagePyMySQL, 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:

  1. Wrap the vendor’s C client library. libpq for PostgreSQL, libmysqlclient or 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.
  2. 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.
  3. 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 -binary suffix 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 or async name (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#

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#

DatabaseSyncAsync
PostgreSQLpsycopg2, psycopg 3, pg8000psycopg 3 (async classes), asyncpg
MySQL / MariaDBmysqlclient, PyMySQLaiomysql, asyncmy
SQLitesqlite3 (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#

  1. Interface: PEP 249 DB-API, or its own API.
  2. Implementation: C binding to the vendor library, pure Python, or its own compiled extension.
  3. Install path: which platforms get a binary wheel, and what a source build needs.
  4. Async story: none, a separate package, or native.
  5. License.
  6. Durability: release cadence and the stall test (open pull requests against merged ones).
  7. 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#

DriverLatestReleasedInterfaceImplementationAsyncLicense
mysqlclient2.3.02026-09-14DB-API (MySQLdb)C binding to libmysqlclient or MariaDB Connector/CNoGPL-2.0-or-later
PyMySQL1.2.32026-09-17DB-APIPure PythonNoMIT
aiomysql0.3.22025-10-22DB-API-like, awaitableBuilt on PyMySQLYesMIT
asyncmy0.2.152026-09-21DB-API-like, awaitableCython, fork of aiomysql and PyMySQL ideasYesApache-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_password needs either TLS or an RSA key exchange; PyMySQL and aiomysql need the cryptography package 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#

DriverLatestReleasedInterfaceImplementationAsyncLicense
psycopg22.9.132026-09-09DB-APIC binding to libpqNoLGPL with exceptions
psycopg 33.3.62026-09-18DB-API (with extensions)Python, optional C or binary accelerator, libpqYesLGPL-3.0
asyncpg0.31.02025-11-24Own APICython, own protocol implementationNative onlyApache-2.0
pg80001.31.52025-09-14DB-APIPure PythonNoBSD-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 execute when parameters are used and does not work for SET or NOTIFY (source: psycopg 3 docs, “Differences from psycopg2”, accessed 2026-10-05).
  • Native async. AsyncConnection and AsyncCursor sit 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 accelerator psycopg[c] (built from source, needs headers), and psycopg[binary] (prebuilt C accelerator with its own libpq). 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?#

SyncAsync
PostgreSQLpsycopg 3 (psycopg2 for existing code)psycopg 3 async classes, or asyncpg
MySQL / MariaDBmysqlclient (C) or PyMySQL (pure Python)asyncmy, or aiomysql
SQLitesqlite3aiosqlite

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?#

EnvironmentWorks without a compiler
Linux, macOS, Windows with PyPI wheelspsycopg[binary], psycopg2-binary, asyncpg, asyncmy, PyMySQL, pg8000, aiomysql
Linux or macOS, plain package namePyMySQL, pg8000, psycopg (pure Python, needs system libpq)
Needs headers and a compilerpsycopg2, 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#

DriverLatestInterfaceImplementationAsyncLicense
sqlite3ships with PythonDB-APIC module in the standard libraryNoPython license
aiosqlite0.22.1 (2025-12-23)DB-API-like, awaitableWraps sqlite3, one worker thread per connectionYesMIT (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 an autocommit attribute that gives PEP 249-conformant behavior when set to False and leaves the legacy behavior as the default (source: Python sqlite3 documentation, accessed 2026-10-05).
  • Thread rule. A connection is bound to its creating thread unless check_same_thread=False is passed, and then the caller owns the locking.
  • Library version. The SQLite version is whatever the Python build links, so features such as RETURNING or STRICT tables 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 sqlite3 and 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#

LevelWhat it settlesCostRungCoverage
L0 registry factsversions, licenses, wheel platforms, stall testminutescitedall drivers
L1 install footprint and import timewhat each package adds to an environment; whether a plain install buildsminutesmeasured-localall profiled drivers; two source builds fail here
L2 round trips against a local serverrelative cost per operation across driversabout 30 minutesmeasured-local11 driver configurations; mysqlclient excluded (source build fails)
deferreda second CPU architecture, a remote server with real network latency, TLS, large concurrencydaysnot runnone

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 uv into 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 fsync and synchronous_commit off. 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 t has 100,000 rows of an integer key, a 29-character string and a float; t2 receives inserts.

Workloads#

NameWhat it does
connectopen and close 200 connections, reported per connect
point5,000 sequential single-row primary-key SELECTs, reported per query
fetch100kSELECT all three columns of all 100,000 rows and fetch them all
insert20k20,000 rows through the driver’s executemany, one transaction
concurrentasync 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:

ConstantMeaningValues seen
apilevelDB-API version“2.0”
threadsafety0 none, 1 threads may share the module, 2 may share connections, 3 may share cursorsvaries by driver
paramstyleplaceholder 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; sqlite3 opened transactions only around data-changing statements until Python 3.12 added an opt-in autocommit attribute).
  • Placeholders. %s against ? against $1. A query written for one driver needs editing for another; ORMs and query builders hide this.
  • Type adaptation. What a numeric column 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#

DriverWhere parameters are bound
psycopg2Client: the driver builds the final SQL string and sends it as a simple query
psycopg 3Server: query text and parameters sent separately through the extended protocol
asyncpgServer: always the extended protocol with a prepared statement
pg8000Server: extended protocol
PyMySQL, mysqlclient, aiomysql, asyncmyClient: escaped and interpolated into the SQL text by default
sqlite3In 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#

DriverIn-process pool
psycopg 3psycopg_pool (separate package, psycopg[pool], 3.3.3), sync and async
psycopg2psycopg2.pool classes in the main package: simple, threaded
asyncpgBuilt in (create_pool)
aiomysqlBuilt in
asyncmyBuilt in
pg8000, PyMySQL, mysqlclient, sqlite3None; 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#

DriverShape
psycopg 3AsyncConnection and AsyncCursor, same method names as sync with await
asyncpgOwn interface: fetch, fetchrow, fetchval, execute, executemany, copy_*
aiomysql, asyncmyCursor interface with await
aiosqliteCursor 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).

PackageReleaseWheelsLinuxmacOSWindowsSource dist
psycopg22.9.136nonenone6yes
psycopg2-binary2.9.13666 aarch64, 6 x86_64, 24 musl126yes
psycopg3.3.61 universalanyanyanyyes
psycopg-binary3.3.6666 aarch64, 6 x86_64, 24 musl126no
psycopg-c3.3.60build onlybuild onlybuild onlyyes
asyncpg0.31.0567 aarch64, 7 x86_64, 14 musl1414yes
pg80001.31.51 universalanyanyanyyes
mysqlclient2.3.016nonenone16yes
PyMySQL1.2.31 universalanyanyanyyes
aiomysql0.3.21 universalanyanyanyyes
asyncmy0.2.15648 aarch64, 8 x86_64, 16 musl1616yes
aiosqlite0.22.11 universalanyanyanyyes
mariadb1.1.1412nonenone12yes
mysql-connector-python26.7.0255 aarch64, 5 x86_64104yes

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):

  • psycopg2 from source failed at the C compile step; pg_config reports an include directory that does not contain libpq-fe.h.
  • mysqlclient from source failed at configuration with “Can not find valid pkg-config name” and the instruction to set MYSQLCLIENT_CFLAGS and MYSQLCLIENT_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)

ConfigurationAdded disk (KB)DistributionsImport (ms)
sqlite3 (stdlib)006.2
aiosqlite55139.1
PyMySQL144125.1
aiomysql (and PyMySQL)358251.3
psycopg (pure Python)9042135.1
pg80001,227582.7
asyncpg13,040165.5
psycopg2-binary16,347116.8
psycopg[binary]25,4113132.3
psycopg[binary,pool]25,5374152.0
asyncmy32,857186.3
mysql-connector-python66,770169.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 importtime put the psycopg.pq subpackage 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 for pass), 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#

  1. 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 -binary packages, which bundle libpq and libssl, and the drivers with no C-library dependency.

  2. 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-built psycopg[c] as the shared-library alternative.

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

  4. executemany is a round-trip question. psycopg2’s plain executemany took 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 async executemany was an unexplained exception at 1.1 s.

  5. 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#

SituationDefault
PostgreSQL, sync or mixed sync/async, new codepsycopg 3 with psycopg[binary] (source-built psycopg[c] where system libraries must be shared)
PostgreSQL, async only, throughput matters, no DB-API needasyncpg
PostgreSQL, no native code allowedpg8000 (slow) or psycopg pure Python
PostgreSQL, existing psycopg2 codestay on psycopg2 built from source; migrate when bulk writes or async matter
MySQL or MariaDB, syncPyMySQL for zero-install, mysqlclient where the headers are managed
MySQL or MariaDB, asyncasyncmy; aiomysql as the established alternative with the stall signal
SQLitesqlite3; 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#

ConfigurationVersionconnect (ms)point query (us)fetch 100k rows (ms)insert 20k rows (ms)10x500 concurrent (ms)
psycopg2-binary2.9.1310.65299311,155n/a
psycopg[binary] sync3.3.612.5595126214n/a
psycopg pure Python sync3.3.610.2598556424n/a
psycopg[binary] async3.3.610.8807631,126433
asyncpg0.31.032.575095159399
pg80001.31.529.31,60825237,561n/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)#

ConfigurationVersionconnect (ms)point query (us)fetch 100k rows (ms)insert 20k rows (ms)10x500 concurrent (ms)
PyMySQL1.2.329.7621285304n/a
aiomysql0.3.22.8671381258432
asyncmy0.2.153.380768237413

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)#

Configurationconnect (ms)point query (us)fetch 100k rows (ms)insert 20k rows (ms)10x500 concurrent (ms)
sqlite30.0216.67710.9n/a
aiosqlite 0.22.10.702399616.82,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#

PersonaSituationWhat eliminates optionsWhat remains
Web app migrating from psycopg2 to psycopg 3Sync framework app, PostgreSQL, existing SQLAnything not DB-API compatible (asyncpg)psycopg 3 binary or source-built
High-throughput async serviceasyncio, PostgreSQL, many concurrent requestsSync drivers, pg8000asyncpg, psycopg 3 async
Data script on SQLiteOne file, one process, pandas or plain SQLAnything with a server or a compile stepsqlite3
MySQL shopMySQL or MariaDB, mixed sync and async codeDrivers needing system headers where the base image is slimPyMySQL, asyncmy, mysqlclient
Locked-down or minimal imageNo compiler, read-only filesystem, or a size capSource builds, large bundled wheelspure-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#

PersonaPickAvoid
Web app migrating from psycopg2psycopg 3 (psycopg[binary] in rebuilt containers, psycopg[c] on shared hosts)asyncpg for a migration that must stay DB-API
High-throughput async serviceasyncpg for PostgreSQL-only and speed; psycopg 3 async plus psycopg_pool for DB-API or mixed sync/asyncA pooler in transaction mode with asyncpg’s default statement cache
Data script on SQLitesqlite3aiosqlite unless the script is already async
MySQL shopasyncmy (async), PyMySQL (sync, zero install), mysqlclient where headers are managedmysqlclient on slim images without a headers step; aiomysql for new work while its stall signal stands
Minimal or locked-down imagePyMySQL, pg8000, sqlite3; psycopg pure Python if libpq is presentAnything 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#

OptionFits when
asyncpgThe 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_poolThe 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#

DatabaseDriverDisk addedImport (net)
PostgreSQLpg80001.2 MB, 5 packages83 ms
PostgreSQLpsycopg pure Python0.9 MB, needs system libpq135 ms
MySQLPyMySQL144 KB25 ms
MySQL, asyncaiomysql358 KB, 2 packages51 ms
SQLitesqlite3none6 ms
SQLite, asyncaiosqlite55 KB39 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#

NeedPickNotes
Sync, installs anywherePyMySQLPure 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 headersmysqlclientC binding; the base image must provide development packages
AsyncasyncmyCython, 64 wheels, Apache-2.0; fastest large fetch measured (68 ms)
Async, establishedaiomysqlMIT; 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 execute with parameters, and parameters in SET and NOTIFY (source: psycopg 3 docs, “Differences from psycopg2”, accessed 2026-10-05).
  • The psycopg2.extras helpers have different equivalents (row factories, executemany with pipelining, COPY as 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#

DeploymentChoice
Container rebuilt on base-image updatespsycopg[binary]: one line, and the bundled libraries refresh with the rebuild
Long-lived host patched by the distributionpsycopg[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.extras use, 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 autocommit attribute gives PEP 249 behavior when set explicitly; the default keeps the legacy implicit-transaction behavior (source: Python sqlite3 docs, accessed 2026-10-05).
  • The SQLite version is the interpreter’s. Check sqlite3.sqlite_version if 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:

  1. 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.
  2. Release cadence, from PyPI.
  3. 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).
  4. Switching cost: how much code is written against the driver’s own interface.
  5. 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#

StanceWhenChoice
ConservativeLong-lived service, small teampsycopg2 built from source, or psycopg 3 binary in rebuilt containers; PyMySQL; sqlite3
Performance-firstAsync, high volume, PostgreSQLasyncpg behind a thin data-access layer; psycopg 3 async if DB-API shape is needed
AdaptiveUnsure which database will winAn 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 sqlite3 under 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).

RepositoryOpen PRsMerged since 2026-04-05Cell
psycopg/psycopg2550healthy
psycopg/psycopg266healthy, low volume
MagicStack/asyncpg3432healthy
PyMySQL/PyMySQL435healthy
PyMySQL/mysqlclient119healthy
long2ice/asyncmy08healthy, nothing waiting
mariadb-corporation/mariadb-connector-python25healthy
aio-libs/aiomysql350stalled
omnilib/aiosqlite160stalled
tlocke/pg8000 (Codeberg)1none found; last commit 2025-09-14finished 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.

Published: 2026-10-05 Updated: 2026-10-05