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

2021-05-23 Iron clad types.

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.)

Fin

May type hints clarify your code,

—Adam


Want better tests? Check out my book Speed Up Your Django Tests which teaches you to write faster, more accurate tests.


Subscribe via RSS, Twitter, or email:

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: mypy, python