asyncpg
High-performance async PostgreSQL driver for Python/asyncio. Implements PostgreSQL binary protocol natively — ~5x faster than psycopg3 in benchmarks. Current version: 0.31.0 (Nov 2025). Still pre-1.0. NOT DB-API 2.0 compliant — uses $1/$2 placeholders not %s. No dict row support out of the box — returns Record objects. Major footgun: prepared statements break with pgbouncer in transaction/statement mode (Supabase, Neon poolers).
Warnings
- breaking Placeholders are $1/$2/$3 (PostgreSQL native) NOT %s (psycopg2) or ? (sqlite3). LLMs trained on psycopg2 code consistently generate %s placeholders which fail with asyncpg.
- breaking asyncpg is NOT DB-API 2.0 compliant. Code written for psycopg2/sqlite3 will not work without changes. No cursor objects, different method names (fetch/fetchrow/fetchval not execute/fetchone/fetchall).
- breaking Prepared statements break with pgbouncer in transaction/statement pool mode. Error: 'prepared statement asyncpg_stmt_X does not exist'. Affects Supabase transaction pooler (port 6543), Neon, and any pgbouncer setup.
- gotcha fetch() returns list of asyncpg.Record objects, not dicts. Record supports dict-style access (row['name']) but isinstance(row, dict) is False. Code that expects dicts breaks silently.
- gotcha Still pre-1.0 (0.31.x). API stability not guaranteed across minor versions.
- gotcha Prepared statements and cursors from Connection.prepare() become invalid once a connection is released back to the pool. Must re-prepare on next acquisition.
Install
-
pip install asyncpg
Imports
- connect
import asyncpg import asyncio async def main(): conn = await asyncpg.connect( 'postgresql://user:pass@localhost/mydb' ) # $1, $2 placeholders — NOT %s row = await conn.fetchrow( 'SELECT id, name FROM users WHERE id = $1', 42 ) print(row['name']) # Record supports dict-style access await conn.close() asyncio.run(main()) - create_pool
import asyncpg import asyncio async def main(): pool = await asyncpg.create_pool( 'postgresql://user:pass@localhost/mydb', min_size=2, max_size=10 ) async with pool.acquire() as conn: rows = await conn.fetch('SELECT * FROM users') for row in rows: print(dict(row)) # convert Record to dict await pool.close() asyncio.run(main())
Quickstart
# pip install asyncpg
import asyncpg
import asyncio
async def main():
# Connection pool for production
pool = await asyncpg.create_pool(
'postgresql://user:pass@localhost/mydb',
min_size=2,
max_size=10
)
async with pool.acquire() as conn:
# $1, $2 — not %s
await conn.execute(
'INSERT INTO users(name, email) VALUES($1, $2)',
'Alice', 'alice@example.com'
)
# fetchrow returns asyncpg.Record — dict-like
row = await conn.fetchrow(
'SELECT * FROM users WHERE name = $1', 'Alice'
)
print(row['name']) # 'Alice'
print(dict(row)) # convert to plain dict
# fetch returns list of Records
rows = await conn.fetch('SELECT id, name FROM users')
await pool.close()
asyncio.run(main())