Search code examples
pythonmypy

Mypy throwing union-attr errors after instance checks before early return


I am trying to implement the chain of responsibility pattern in python by passing a Request object into a handler "chain". The Request object passed in to each handler method in sequence has a few optional fields and some union types.

The general pattern I have for these handlers is to create a few boolean values at the top of the method to check if the current handler method should handle the request and if not, return early by calling the next handler in line.

The following is an example of my setup:


from pathlib import Path
from typing import Any, Callable


class Request:
    data: int | Any | None = None
    location: str | Path | None = None


def handle_int(request: Request, next_handler: Callable[[Request], int]) -> int:
    is_int = isinstance(request.data, int)
    is_str = isinstance(request.location, int)

    if not (is_int and is_str):
        return next_handler(request)

    a = request.location.split("_")

    return request.data * 3

The issue I am having is that mypy catches union-attr errors whenever I access the attributes on the request object:

test_mypy.py:17:9: error: Item "Path" of "str | Path | None" has no attribute "split"  [union-attr]
test_mypy.py:17:9: error: Item "None" of "str | Path | None" has no attribute "split"  [union-attr]
test_mypy.py:19:12: error: Unsupported operand types for * ("None" and "int")  [operator]
test_mypy.py:19:12: note: Left operand is of type "int | Any | None"

I can get rid of these by adding assertions after the return statement but that is creating basically duplicate code for the predicate checks before the early return and feels like a bad pattern.

Apart from creating more specific request types is there a way to let mypy know that the fact we are after the early return means that checks have been done on the form of the attributes n the request already?


Solution

  • mypy is just getting confused because the type narrowing of isinstance cannot be propagated through variables. It has to be done directly in the if.

    Use

    if not (isinstance(request.data, int) and isinstance(request.location, str)):
        return next_handler(request)
    

    Instead of

    is_int = isinstance(request.data, int)
    is_str = isinstance(request.location, str)
    
    if not (is_int and is_str):
        return next_handler(request)