SQLAlchemy and mypy type checking
Have you ever spent some time looking to add type annotations to a Python package to test with mypy? Something I find to be a rather frequent frustration is finding the right type checking configuration and type stubs for external libraries.
In particular libraries that use extensive metaprogramming, as seen in many ORM libraries, tend to not play nicely with mypy. Recently I ran into this with SQLAlchemy. If you don't have the right plugin you see results from mypy like this:
$tox
py38 run-test: commands[0] | mypy --config-file mypy.ini example
example/shared/db/inventory.py:11: error: Module "sqlalchemy.orm" has no attribute "declarative_base"
example/shared/db/inventory.py:122: error: Variable "example.db.inventory.Base" is not valid as a type
example/shared/db/inventory.py:122: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
example/shared/db/inventory.py:122: error: Invalid base class "Base"
These messages are obviously false positives because there's definitely a declarative_base
provided by SQLAlchemy, just mypy doesn't understand it. You need to use a mypy plugin to get results that make sense. But which plugin and configuration to use? Much like how apps are getting worse search results are getting worse, when I searched up type stubs for various packages I found shockingly little of relevance. Adding quotation marks did little except add to the absurdity, as seen for example here:
This post is as much of a note to my future self as anything else. Mypy support landed natively in SQLAlchemy version 1.4 so the best way to deal with this is just to use what's provided by SQLAlchemy. To do this you need to change the installation of SQLAlchemy requirements to add in the optional mypy extra.
pip install sqlalchemy[mypy]
Then you need to set up your mypy to use the plugin. Then in mypy.ini
you need to enable the plugin:
[mypy]
plugins = sqlalchemy.ext.mypy.plugin
This should then get you to the stage where you are getting real type checking results. You may need to add some additional annotations at this point to deal with some loose ends, see the SQLAlchemy mypy extension documentation for more info.