CLOVER🍀

That was when it all began.

mypy+types-PyMySQLでConnectionを扱う時にハマった話

これは、なにをしたくて書いたもの?

こちらのエントリーを書いている時にmypyとtypes-PyMySQLでConnectionを扱う時にちょっとハマったので、単独のエントリーとして
メモしておきます。

PyMySQLでinsert文(またはreplace文)を高速に実行するにはexecutemanyを使う - CLOVER🍀

全然情報がなくて困ったので。

なんの話?

PyMySQLには型アノテーションの定義が含まれておらず、typeshedが提供するtypes-PyMySQLを使うことになります。

types-PyMySQL

こちらのConnectionを扱ってmypyで型チェックをする時にハマりました。

環境

今回の環境はこちら。

$ uv --version
uv 0.5.14


$ python3 --version
Python 3.12.3

MySQLは172.17.0.2でアクセスできるものとします。

 MySQL  localhost:3306 ssl  practice  SQL > select version();
+-----------+
| version() |
+-----------+
| 8.4.3     |
+-----------+
1 row in set (0.0007 sec)

準備

プロジェクトの作成。

$ uv init --vcs none types-pymysql-connection
$ cd types-pymysql-connection

ライブラリーのインストール。

$ uv add PyMySQL[rsa]
$ uv add --dev mypy types-PyMySQL

pyproject.toml

[project]
name = "types-pymysql-connection"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "pymysql[rsa]>=1.1.1",
]

[dependency-groups]
dev = [
    "mypy>=1.14.1",
    "types-pymysql>=1.1.0.20241103",
]

[tool.mypy]
strict = true
disallow_any_unimported = true
disallow_any_expr = true
disallow_any_explicit = true
warn_unreachable = true
pretty = true

インストールされたライブラリーの一覧。

$ uv pip list
Package           Version
----------------- --------------
cffi              1.17.1
cryptography      44.0.0
mypy              1.14.1
mypy-extensions   1.0.0
pycparser         2.22
pymysql           1.1.1
types-pymysql     1.1.0.20241103
typing-extensions 4.12.2

困ったこと

困ったことは2つあります。

最初はcursorclassの指定方法。こんなサンプルを書いてみます。

connect_mysql.py

import pymysql

with pymysql.connect(
        host="172.17.0.2",
        port=3306,
        user="kazuhira",
        password="password",
        database="practice",
        cursorclass=pymysql.cursors.DictCursor
) as connection:
    print(f"auto commit mode = {connection.get_autocommit()}")

cursorclassにはDictCursorを指定しています。これはドキュメントのサンプルに書かれているものそのものです。

Examples — PyMySQL 0.7.2 documentation

もちろん実行できます。

$ uv run connect_mysql.py
auto commit mode = False

ですが、mypyでチェックすると怒られます。

$ uv run mypy .
connect_mysql.py:9: error: Expression type contains "Any" (has type "type[DictCursor]")  [misc]
            cursorclass=pymysql.cursors.DictCursor
                        ^~~~~~~~~~~~~~~~~~~~~~~~~~
Found 1 error in 1 file (checked 1 source file)

Any扱いになってしまうようです。

ここで以下のようにtype[DictCursor]型の変数を作成すると、パスするようになりました。

connect_mysql.py

import pymysql
from pymysql.cursors import DictCursor

DictCursorType: type[DictCursor] = DictCursor

with pymysql.connect(
        host="172.17.0.2",
        port=3306,
        user="kazuhira",
        password="password",
        database="practice",
        #cursorclass=pymysql.cursors.DictCursor
        cursorclass=DictCursorType
) as connection:
    print(f"auto commit mode = {connection.get_autocommit()}")

この部分ですね。クラスオブジェクトの型を定義していることになります。

DictCursorType: type[DictCursor] = DictCursor

typing --- 型ヒントのサポート — Python 3.12.8 ドキュメント

これでmypyのチェックがパスするようになりました。

$ uv run mypy .
Success: no issues found in 1 source file

実行できる点も変わりません。

$ uv run connect_mysql.py
auto commit mode = False

2つ目はConnectionの型を明示的に宣言する場合です。以下のようなサンプルを作ってみます。
type[DictCursor]は含めたものにしています

connect_mysql2.py

import pymysql
from pymysql.cursors import DictCursor
from pymysql.connections import Connection

def connect_mysql() -> Connection:
    DictCursorType: type[DictCursor] = DictCursor

    return pymysql.connect(
        host="172.17.0.2",
        port=3306,
        user="kazuhira",
        password="password",
        database="practice",
        cursorclass=DictCursorType
    )

with connect_mysql() as connection:
    print(f"auto commit mode = {connection.get_autocommit()}")

これも問題なく実行できます。

$ uv run connect_mysql2.py
auto commit mode = False

ところがmypyでチェックすると、Connectionに型パラメーターがないと怒られます。

$ uv run mypy .
connect_mysql2.py:5: error: Missing type parameters for generic type "Connection"  [type-arg]
    def connect_mysql() -> Connection:
                           ^
connect_mysql2.py:8: error: Expression type contains "Any" (has type "Connection[Any]")  [misc]
        return pymysql.connect(
               ^
connect_mysql2.py:17: error: Expression type contains "Any" (has type "Connection[Any]")  [misc]
    with connect_mysql() as connection:
         ^~~~~~~~~~~~~~~
connect_mysql2.py:18: error: Expression type contains "Any" (has type "Connection[Any]")  [misc]
        print(f"auto commit mode = {connection.get_autocommit()}")
                                    ^~~~~~~~~~
Found 4 errors in 1 file (checked 2 source files)

Connectionを扱う部分がほぼNGになりますね。

もちろんPyMySQLのConnectionには型パラメーターなどありません。

Connection Object — PyMySQL 0.7.2 documentation

ここでtypeshedにあるPyMySQLのスタブファイルを見ると、Connectionが型パラメーターを取るようになっています。

class Connection(Generic[_C]):

typeshed/stubs/PyMySQL/pymysql/connections.pyi at main · python/typeshed · GitHub

_CというのはどうやらCursorを指しているようです。

_C = TypeVar("_C", bound=Cursor)

ではこうすればいいのかなと修正してみます。

def connect_mysql() -> Connection[DictCursor]:

mypyのチェックが通るようになりました。

$ uv run mypy .
Success: no issues found in 2 source files

ですが今度は、実行できなくなってしまいました…。添字は付かないはずだ、と怒られています。

$ uv run connect_mysql2.py
Traceback (most recent call last):
  File "/path/to/types-pymysql-connection/connect_mysql2.py", line 5, in <module>
    def connect_mysql() -> Connection[DictCursor]:
                           ~~~~~~~~~~^^^^^^^^^^^^
TypeError: type 'Connection' is not subscriptable

そこで、戻り値の型を文字列にしてみます。

def connect_mysql() -> "Connection[DictCursor]":

こうすることで、mypyのチェックもパスしつつ

$ uv run mypy .
Success: no issues found in 2 source files

実行もできるようになります。

$ uv run connect_mysql2.py
auto commit mode = False

このあたりを通すのにけっこう苦労しました…。

おわりに

types-PyMySQLでConnectionを扱う時にハマった話を書いてみました。

あまり情報がなく、解決するのに苦労しましたね…。

戻り値の型を文字列で宣言する方法があることを忘れていたので、そこが盲点だったかもしれません。

Pythonで型ヒント(Type Hints)を試してみる(+Mypy) - CLOVER🍀

Pythonのバージョンによっては問題にならないかも?といった話もあるようですが…。