How to Connect to MySQL from Python

7 min readMySQL

Python has three main MySQL drivers -- mysqlclient, PyMySQL, and MySQL Connector/Python -- plus SQLAlchemy as an abstraction on top, and aiomysql/asyncmy for asyncio. They all get the job done; they differ in installation friction, speed, and async support. This guide shows working code for each and the trade-offs that decide between them.

Which driver should you use?

DriverImplementationAsyncNotes
mysqlclientC extension (libmysqlclient)NoFastest sync driver; needs build tools or wheels
PyMySQLPure PythonNoInstalls anywhere, no compilation, slower on big result sets
MySQL Connector/PythonPure Python + optional C extensionNoOracle's official driver, GPLv2 with FOSS exception
aiomysqlPure Python (reuses PyMySQL)YesThe established asyncio option
asyncmyCython (reuses PyMySQL/aiomysql)YesFaster asyncio fork of aiomysql

Rules of thumb: mysqlclient when you control the environment and want raw speed (it wraps the C client library, so it is the fastest at fetching large result sets). PyMySQL when you want zero installation drama -- pure Python, works on any platform, and is a drop-in replacement for mysqlclient's API. MySQL Connector/Python when you want the vendor-supported driver or features like the X DevAPI. For asyncio, aiomysql or asyncmy. All three sync drivers implement DB-API 2.0, so the query code looks nearly identical; switching later is cheap.

These drivers also speak to MariaDB, which uses the same wire protocol. (MariaDB additionally ships its own mariadb connector.)

Connection details you need

Host, port (3306 by default), database name, user, password. The examples read from environment variables -- never hardcode credentials.

PyMySQL

pip install PyMySQL
import os
import pymysql
 
conn = pymysql.connect(
    host=os.environ["MYSQL_HOST"],
    port=3306,
    user=os.environ["MYSQL_USER"],
    password=os.environ["MYSQL_PASSWORD"],
    database=os.environ["MYSQL_DATABASE"],
    charset="utf8mb4",
    cursorclass=pymysql.cursors.DictCursor,
)
 
try:
    with conn.cursor() as cur:
        cur.execute(
            "SELECT id, email FROM users WHERE created_at > %s",
            ("2026-01-01",),
        )
        for row in cur.fetchall():
            print(row["id"], row["email"])
    conn.commit()
finally:
    conn.close()

Three details worth knowing:

  • charset="utf8mb4": always set this. MySQL's legacy utf8 charset is a 3-byte subset that cannot store emoji or many CJK characters; utf8mb4 is actual UTF-8.
  • autocommit is off by default (per DB-API). Writes are invisible to other connections until you call conn.commit(). Forgetting this is the single most common "my INSERT disappeared" report. Pass autocommit=True if you want MySQL's native behavior.
  • Parameters use %s regardless of type -- it is not Python string formatting, the driver escapes values for you. Never build SQL with f-strings.

mysqlclient

The C-extension descendant of the old MySQLdb project, and the driver Django's MySQL backend recommends.

pip install mysqlclient

On Linux you may need headers first (default-libmysqlclient-dev on Debian/Ubuntu); macOS users typically brew install mysql-client pkg-config. Recent versions ship binary wheels for common platforms, which removes most of the historical pain.

import MySQLdb
 
conn = MySQLdb.connect(
    host="localhost",
    user="app",
    password="...",
    database="mydb",
    charset="utf8mb4",
)
cur = conn.cursor()
cur.execute("SELECT id, email FROM users WHERE id = %s", (42,))
print(cur.fetchone())
conn.commit()
conn.close()

Same DB-API shape as PyMySQL -- intentionally so. PyMySQL was written as a pure-Python drop-in for this exact API. If you start on PyMySQL and later need mysqlclient's speed, the migration is mostly the import line.

MySQL Connector/Python

Oracle's official driver. Pure Python by default with an optional C extension; licensed GPLv2 with the FOSS exception (fine for most uses, but check if you ship proprietary software -- the other two drivers are MIT/dual-licensed and avoid the question).

pip install mysql-connector-python
import mysql.connector
 
conn = mysql.connector.connect(
    host="localhost",
    user="app",
    password="...",
    database="mydb",
)
cur = conn.cursor(dictionary=True)
cur.execute("SELECT id, email FROM users WHERE id = %s", (42,))
print(cur.fetchone())
cur.close()
conn.close()

It also ships a built-in pool:

from mysql.connector import pooling
 
pool = pooling.MySQLConnectionPool(pool_name="app", pool_size=5, host="localhost", user="app", password="...", database="mydb")
conn = pool.get_connection()  # returns to the pool on conn.close()

Async: aiomysql and asyncmy

Both reuse PyMySQL's protocol implementation. aiomysql is the established choice; asyncmy rewrites the hot paths in Cython for speed and is the one SQLAlchemy's docs point to for async MySQL.

pip install aiomysql
import asyncio
import aiomysql
 
async def main():
    pool = await aiomysql.create_pool(
        host="localhost", user="app", password="...", db="mydb", autocommit=True
    )
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute("SELECT count(*) FROM users")
            print(await cur.fetchone())
    pool.close()
    await pool.wait_closed()
 
asyncio.run(main())

SQLAlchemy

Not a driver -- it needs one of the above underneath, selected by the URL scheme:

pip install sqlalchemy mysqlclient
import os
from sqlalchemy import create_engine, text
 
# mysql+mysqldb:// for mysqlclient, mysql+pymysql:// for PyMySQL,
# mysql+mysqlconnector:// for Connector/Python, mysql+asyncmy:// for async
engine = create_engine(
    "mysql+mysqldb://app:password@localhost:3306/mydb?charset=utf8mb4",
    pool_size=5,
    max_overflow=10,
    pool_recycle=3600,
    pool_pre_ping=True,
)
 
with engine.connect() as conn:
    result = conn.execute(text("SELECT id, email FROM users WHERE id = :id"), {"id": 42})
    print(result.fetchone())

pool_recycle and pool_pre_ping are not optional decoration for MySQL -- see the next section.

Connection pooling and the wait_timeout trap

MySQL servers drop idle connections after wait_timeout seconds (28800 by default, but cloud providers and DBAs often set it much lower). A pooled connection that sat idle past the timeout produces the classic:

MySQL server has gone away (error 2006)

on its next query. Defenses, in order of preference:

  1. pool_pre_ping=True (SQLAlchemy): emits a lightweight ping before handing out a pooled connection and transparently replaces dead ones.
  2. pool_recycle=3600: proactively retires connections older than N seconds. Set it below your server's wait_timeout.
  3. With raw drivers, conn.ping(reconnect=True) (PyMySQL/mysqlclient) before use accomplishes the same manually.

For serverless or many-instance deployments where in-process pools multiply into thousands of connections, put ProxySQL or your cloud provider's pooler (e.g. RDS Proxy) in front of the server.

SSL/TLS

Hosted MySQL (RDS, Cloud SQL, PlanetScale, Aiven) generally requires TLS. With PyMySQL:

conn = pymysql.connect(..., ssl={"ca": "/path/to/ca.pem"})

With SQLAlchemy, append ?ssl_ca=/path/to/ca.pem to the URL (parameter names vary slightly per underlying driver). Passing a CA bundle enables server certificate verification; connecting with TLS but no CA check encrypts the traffic without proving you reached the right server.

Common errors and what they mean

  • (2003) Can't connect to MySQL server: wrong host/port, server down, or firewall. On Linux servers also check bind-address in MySQL config -- 127.0.0.1 means no remote connections.
  • (1045) Access denied for user: bad credentials, or the user exists for a different host pattern ('app'@'localhost' is a different account than 'app'@'%').
  • (1049) Unknown database: the database name in your connection call does not exist; SHOW DATABASES to verify.
  • (2006) MySQL server has gone away: idle timeout (see pooling section) or a query exceeding max_allowed_packet.
  • (1156) caching_sha2_password auth errors on MySQL 8+: old driver versions lack the default auth plugin. Upgrade the driver; switching the user to mysql_native_password is the legacy workaround, not the fix.

Verifying your connection and exploring the data

After connecting, you usually want to see the schema and check your data landed. The mysql CLI with SHOW TABLES works; a GUI is faster for browsing -- we compared the options in Best MySQL GUI Clients. For loading data, see Import CSV to MySQL and Import JSON to MySQL. When you start writing real queries, the MySQL window functions and JSON queries guides cover the highest-leverage SQL features.

Version notes (as of June 2026): mysqlclient 2.2.x, PyMySQL 1.2.x, MySQL Connector/Python 9.x, aiomysql 0.3.x, asyncmy 0.2.x.

Mako connects to MySQL with AI-powered autocomplete for writing queries. Try it free at mako.ai.

Mako — open source

Skip the terminal. Use Mako.

Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.