kenyan programmer

mostly about programming


Help! My client code isn't being type checked by mypy

That was the issue I had this morning. I had written some piece of code with type annotations but obviously wrong type assignments in unit tests, but mypy was reporting that everything was OK.

I had code like this:

# lib.py
from dataclasses import dataclass


@dataclass
class Question:
    text: str
    options: list['Option'] | None = None


@dataclass
class Option:
    text: str
    correct: bool = False

And a test like this:

# test_lib.py
import unittest
import lib


class QuestionTestCase(unittest.TestCase):
    def test_create_question(self):
        q = lib.Question(text=1)
        q.options = 2


if __name__ == "__main__":
    unittest.main()

Type checking with mypy gave me no errors.

$ mypy lib.py test_lib.py 
Success: no issues found in 2 source files

If you can follow the code, type checking the test code should produce errors as I’m assigning an integer to Question.text instead of a a string, and similarly, Question.options should be optional list of Option, whereas I assigned an integer.

A bit of digging, and I found out that I had to add annotations to the test methods as well like below. This would signal to mypy that I wanted to type check the method as well.

...

class QuestionTestCase(unittest.TestCase):
    def test_create_question(self) -> None:  # annotate here
        q = lib.Question(text=1)
        q.options = 2

...

mypy should now show errors.

$ mypy lib.py test_lib.py
test_lib.py:7: error: Argument "text" to "Question" has incompatible type "int"; expected "str"  [arg-type]                                                                                              
test_lib.py:8: error: Incompatible types in assignment (expression has type "int", variable has type "list[Option] | None")  [assignment]                                                                    
Found 2 errors in 1 file (checked 2 source files)

Another option you can use, if you don’t want to go annotating all tests you already have, is to run mypy with the flag --check-untyped-defs or add it in pyproject.toml.

$ mypy --check-untyped-defs lib.py test_lib.py               
test_lib.py:7: error: Argument "text" to "Question" has incompatible type "int"; expected "str"  [arg-type]                                                                                              
test_lib.py:8: error: Incompatible types in assignment (expression has type "int", variable has type "list[Option] | None")  [assignment]                                                                    
Found 2 errors in 1 file (checked 2 source files)

This is explained on the page of Common Issues and solutions. The whole of it is worth reading.

I don’t know why I haven’t encountered this issue before over the years, probably because the projects I’ve worked on didn’t type check unit tests or already had check-untyped-defs set.

It’s probably useful every once in a while setting up a small projects by hand from scratch to identify such kinds of issues.