mysql-connector-python
MySQL Connector/Python is Oracle's own MySQL driver for Python. It speaks the MySQL client/server protocol directly and exposes it through the standard DB API 2.0 interface (PEP 249), so you get connect(), cursors, execute(), fetchone() and commit() exactly as with any other Python database driver. The package ships two implementations behind the same API: a pure Python one that needs nothing but the interpreter, and a compiled C extension that is bundled in the wheels and used automatically when it is available. Because it comes from the MySQL team, it tracks server features closely: the enterprise authentication plugins, the vector type, connection pooling, an asyncio interface under mysql.connector.aio, and optional OpenTelemetry tracing all live in this one package. You install mysql-connector-python but you import mysql.connector.
The right pick when you need Oracle's authentication plugins, server-version parity or first-party telemetry, and an acceptable pick everywhere else since the pure Python fallback installs anywhere. If none of those apply, PyMySQL is lighter, MIT-licensed and moves faster.
Use it if
- You need an authentication plugin the community drivers do not implement: LDAP SASL, Kerberos through GSSAPI, WebAuthn or FIDO2 devices, or the OpenID Connect JWT support added in 9.1.0
- You want a driver whose release train follows the MySQL server itself, so new server features such as the vector type land in the client without waiting on a volunteer maintainer
- You want prebuilt wheels with a C extension for speed but a pure Python fallback for exotic platforms, with no MySQL client libraries or compiler required either way
- You need first-party OpenTelemetry spans around queries, which the telemetry extra installs and wires up for you
- Licensing matters to you: this is GPLv2 with the MySQL FOSS License Exception, not a permissive license, so shipping it inside a closed-source product is a question for your legal team; PyMySQL is MIT and does the same job for ordinary queries
- You want fast turnaround on bugs: development happens inside Oracle, bug reports live at bugs.mysql.com rather than the GitHub mirror (which shows a single open issue), contributions need the Oracle Contributor Agreement, and releases come roughly quarterly, so a fix can be a full quarter away
- You run on musl or an older glibc: the C extension wheels target manylinux_2_28, macOS 14 and win_amd64 on CPython 3.10 through 3.14, so Alpine images silently fall back to the pure Python universal wheel and the speed argument disappears
- Your dependency graph is tight: every optional extra pins exact versions (dnspython==2.6.1, gssapi==1.8.3, fido2==1.1.2, the OpenTelemetry trio at ==1.33.1), which conflicts with anything else in your app that needs a different OpenTelemetry or dnspython
- You are writing asyncio code with heavy concurrency: mysql.connector.aio is newer and far less exercised in production than asyncmy or aiomysql, and the driver's async pooling arrived only recently as a community contribution
Setup reality
pip install mysql-connector-python needs Python 3.10 or newer and pulls no required dependencies at all, which is the nicest thing about it. The naming is where people lose an hour: the distribution is mysql-connector-python, the import is mysql.connector, and the old mysql-connector name on PyPI is a stale unrelated upload you should not install. The X DevAPI (document store) now lives in a separate distribution called mysqlx-connector-python, so import mysqlx will fail on a plain install. Extras are installed as mysql-connector-python[telemetry], [dns-srv], [gssapi] or [webauthn], and each one pins its dependency to an exact version, so expect a resolver fight if your app already uses OpenTelemetry. The C extension ships inside the platform wheels and is used by default; pass use_pure=True to connect() to force the Python implementation, and check mysql.connector.HAVE_CEXT to see which one you actually got, because a pure Python fallback looks identical until you profile it. Two runtime defaults bite newcomers: autocommit is False, so nothing you write persists without cnx.commit(), and unbuffered cursors are the default, so a second query on the same cursor before draining the first raises InternalError: Unread result found.
Patterns
Connect and run a query with context managersconnect-and-query
import mysql.connector
with mysql.connector.connect(
host="127.0.0.1",
port=3306,
user="app",
password="s3cr3t",
database="shop",
) as cnx:
with cnx.cursor() as cur:
cur.execute("SELECT id, email FROM users LIMIT 5")
for row in cur:
print(row)Both the connection and the cursor support with-blocks; the connection block closes the socket but does not commit, so a write still needs cnx.commit() before the block ends. Set use_pure=True in connect() if you want the pure Python implementation instead of the bundled C extension.
Pass parameters instead of formatting SQLparameterized-query
cur.execute(
"SELECT id FROM users WHERE email = %s AND active = %s",
("ada@example.com", True),
)
# named style also works, with a dict
cur.execute(
"UPDATE users SET plan = %(plan)s WHERE id = %(id)s",
{"plan": "pro", "id": 42},
)The placeholder is always %s even for numbers and dates; %d is not supported and raises a formatting error. Never build SQL with f-strings: the driver escapes values for you, and CVE-level injection bugs in this project's history came from bypassing that path.
Get rows back as dicts instead of tuplesdictionary-cursor
with cnx.cursor(dictionary=True) as cur:
cur.execute("SELECT id, email, plan FROM users WHERE id = %s", (42,))
user = cur.fetchone()
print(user["email"], user["plan"])dictionary=True costs a little per row but removes the column-order coupling that breaks silently when someone edits the SELECT list. Named-tuple and raw prepared cursors were removed in 9.3.0, so dictionary and buffered are the flags worth knowing.
Commit, roll back, and know the autocommit defaulttransactions
cnx = mysql.connector.connect(user="app", password="s3cr3t", database="shop")
try:
cnx.start_transaction(isolation_level="REPEATABLE READ")
with cnx.cursor() as cur:
cur.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", (100, 1))
cur.execute("UPDATE accounts SET balance = balance + %s WHERE id = %s", (100, 2))
cnx.commit()
except mysql.connector.Error:
cnx.rollback()
raise
finally:
cnx.close()autocommit defaults to False, so writes that are never committed vanish on close with no warning at all. Pass autocommit=True to connect() if you want per-statement commits, and note that DDL such as CREATE TABLE commits implicitly on the server regardless.
Insert many rows in one round tripbulk-insert
rows = [("ada", "ada@example.com"), ("linus", "linus@example.com")]
with cnx.cursor() as cur:
cur.executemany(
"INSERT INTO users (name, email) VALUES (%s, %s)",
rows,
)
cnx.commit()
print(cur.rowcount)executemany rewrites INSERT statements into MySQL multi-row syntax, which is where the speedup comes from; for UPDATE or DELETE it just loops and is no faster than your own loop. Results of each statement are discarded, so do not use it when you need lastrowid per row.
Use a connection pool in a web appconnection-pool
from mysql.connector.pooling import MySQLConnectionPool
pool = MySQLConnectionPool(
pool_name="app",
pool_size=5,
pool_reset_session=True,
host="127.0.0.1",
user="app",
password="s3cr3t",
database="shop",
)
cnx = pool.get_connection() # blocks or raises PoolError when exhausted
try:
with cnx.cursor() as cur:
cur.execute("SELECT 1")
cur.fetchall()
finally:
cnx.close() # returns the connection to the pool, does not close the socketclose() on a pooled connection returns it to the pool rather than closing it, so forgetting the finally block leaks the slot permanently. pool_size is capped at 32, and a pool created through connect(pool_name=...) is reused globally by name, so two different configs sharing a name will surprise you.
Query from asyncio codeasync-queries
import asyncio
from mysql.connector.aio import connect
async def main():
async with await connect(
host="127.0.0.1", user="app", password="s3cr3t", database="shop"
) as cnx:
async with await cnx.cursor(dictionary=True) as cur:
await cur.execute("SELECT id, email FROM users LIMIT 5")
print(await cur.fetchall())
asyncio.run(main())Note the double keyword: connect() and cursor() return awaitables, so it is async with await, not plain async with. The async implementation is pure Python only, so the C extension speed advantage does not apply here.
Fix the Unread result found errorunread-result
# fails: the first result set was never drained
with cnx.cursor() as cur:
cur.execute("SELECT id FROM users")
cur.execute("SELECT id FROM orders") # InternalError: Unread result found
# option 1: buffer the rows inside execute()
with cnx.cursor(buffered=True) as cur:
cur.execute("SELECT id FROM users")
cur.execute("SELECT id FROM orders")
# option 2: drain before reusing the cursor
with cnx.cursor() as cur:
cur.execute("SELECT id FROM users")
cur.fetchall()
cur.execute("SELECT id FROM orders")Cursors are unbuffered by default, meaning rows stay on the wire until you fetch them. buffered=True (per cursor, or on connect() for all cursors) reads everything into memory up front, which fixes the error but is a bad idea for large result sets.
Use server-side prepared statements for a hot queryprepared-statements
with cnx.cursor(prepared=True) as cur:
stmt = "SELECT id, email FROM users WHERE plan = %s AND active = %s"
for plan in ("free", "pro", "team"):
cur.execute(stmt, (plan, True))
print(plan, cur.fetchall())The statement is prepared once on the server the first time and reused for later execute() calls with the same SQL, so it only pays off in loops. Prepared cursors do not accept multi-statement SQL and raise ProgrammingError if you try.
Require TLS and verify the server certificatetls-connection
cnx = mysql.connector.connect(
host="db.internal",
user="app",
password="s3cr3t",
database="shop",
ssl_ca="/etc/ssl/certs/ca.pem",
ssl_verify_identity=True,
tls_versions=["TLSv1.3"],
)
print(cnx.get_server_info())Setting ssl_ca alone verifies the chain but not the hostname; ssl_verify_identity=True is what checks the certificate matches the host you dialed. ssl_disabled=True turns encryption off entirely and should never appear in production config.
Run several statements in one execute callmulti-statement-script
script = (
"CREATE TEMPORARY TABLE t (id INT); "
"INSERT INTO t VALUES (1), (2); "
"SELECT COUNT(*) FROM t;"
)
with cnx.cursor() as cur:
cur.execute(script, map_results=True)
while True:
if cur.with_rows:
print(cur.statement, cur.fetchall())
if cur.nextset() is None:
breakThe old execute(..., multi=True) generator was removed in 9.2.0; execute() now splits the script itself and you walk the result sets with nextset(). map_results=True is what makes cursor.statement report the individual statement each result came from instead of the whole script.
Branch on MySQL error codes instead of message texterror-handling
import mysql.connector
from mysql.connector import errorcode
try:
cnx = mysql.connector.connect(user="app", password="wrong", database="shop")
except mysql.connector.Error as err:
if err.errno == errorcode.ER_ACCESS_DENIED_ERROR:
raise SystemExit("bad credentials")
if err.errno == errorcode.ER_BAD_DB_ERROR:
raise SystemExit("database does not exist")
raiseEvery exception carries errno, sqlstate and msg, and the errorcode module holds the named constants, so you never have to match on English error strings. All driver exceptions inherit from mysql.connector.Error, which is the one class to catch at a boundary.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| PyMySQL | PyPI | You want an MIT-licensed pure Python driver with no license questions and no Oracle release train, and you do not need the enterprise auth plugins. |
| mysqlclient | PyPI | You want the fastest option for Django or SQLAlchemy and can install libmysqlclient plus a compiler in your build image. |
| aiomysql | PyPI | Your application is asyncio-first and you want an async driver with a pool that has been in production use for years. |
| asyncmy | PyPI | You need async MySQL access with Cython-level throughput and are willing to depend on a smaller community project. |