Python Type Hints - Mypy doesn’t allow variables to change type

Mypy does not allow variables to change type. I found this a little bit of a shock at first, but after I adapted, I found my code was more readable.
For example, take this snippet of a Django view I tried to write in DB Buddy:
from django.http import HttpRequest, HttpResponse
def avatar(request: HttpRequest) -> HttpResponse:
size: str = request.GET.get("size", "")
try:
size = int(size)
except ValueError:
size = 40
...
return HttpResponse(...)
The code fetches the size
query parameter, which is a string, and tries to parse it as an int
, or fall back to the default value 40
. Python can run this code just fine, but if we check it with Mypy, we’ll see some errors:
$ mypy example.py
example.py:7: error: Incompatible types in assignment (expression has type "int", variable has type "str")
example.py:9: error: Incompatible types in assignment (expression has type "int", variable has type "str")
Found 2 errors in 1 file (checked 1 source file)
We might try to fix the errors by explicitly declaring that size
is changing type:
size: str = request.GET.get("size", "")
try:
size: int = int(size)
except ValueError:
size = 40
…but Mypy does not allow redefinition:
$ mypy example.py
example.py:7: error: Name 'size' already defined on line 5
example.py:9: error: Incompatible types in assignment (expression has type "int", variable has type "str")
Found 2 errors in 1 file (checked 1 source file)
We might also try declaring size
with the union of types it takes, str
and int
:
size: str | int = request.GET.get("size", "")
This can work in some cases, but not generally. Mypy can use type narrowing to figure out when size
must be an int
, but this isn’t always possible. In other cases we will have to use type narrowing or cast()
to tell Mypy we know the type is int
after the parsing, to allow int
-only opertions. Such function calls are unnecessary noise in our code.
The best solution is to use two variables, the first a str
and the second an int
:
from django.http import HttpRequest, HttpResponse
def avatar(request: HttpRequest) -> HttpResponse:
size_param: str = request.GET.get("size", "")
try:
size = int(size_param)
except ValueError:
size = 40
...
return HttpResponse(...)
This makes Mypy pass:
$ mypy example.py
Success: no issues found in 1 source file
This may seem like a bit more work, but it clarifies the code, for both Mypy and human readers. When we see the name size
later in the function, we know it’s an int
from its first mention, and don’t have to follow a “type lifecycle” through the function.
(Note: there is a configuration value called allow_redefinition
that allows type redefinition in some contexts. But I wouldn’t recommend turning it on. It doesn’t help in the example here anyway.)
If your Django project’s long test runs bore you, I wrote a book that can help.
One summary email a week, no spam, I pinky promise.
Related posts: