Skip to content

Tips to make Mypy happy

Romain Hugonnet edited this page Sep 20, 2022 · 7 revisions

Here are some of the most common MyPy problems we often run into, the examples might avoid a long search on the web. A summary of points to quickly get to the details:

  1. How to define overloading of a function when mypy complains about different possible outputs;
  2. How to order overloading definitions to avoid "overloading signature X will never be matched";
  3. How to deal with overloading dependent on a Literal (e.g. tag=True) for their output;
  4. The special case of overloading based on a parameter located after a default argument;

In detail:

  1. You have defined a function, but the output depends on the input?
def curvature(
    dem: np.ndarray | np.ma.masked_array | RasterType,
    resolution: float | tuple[float, float] | None = None,
    use_richdem: bool = False,
) -> np.ndarray | Raster:

You can specify this with @overloading directly before the function, as follows:

@overload
def curvature(
    dem: RasterType,
    resolution: float | tuple[float, float] | None,
    use_richdem: bool = False,
) -> Raster: ...

@overload
def curvature(
    dem: np.ndarray | np.ma.masked_array,
    resolution: float | tuple[float, float] | None,
    use_richdem: bool = False,
) -> np.ndarray: ...

And don't forget: Always repeat all default parameters when making the overload, or you'll have some nasty errors!

  1. Is the above really going to work? Well, actually, no! Order matters! Mypy generally considers custom-defined types such as RasterType as Any. So the first @overload can take Any as input, which includes anything... including the NumPy arrays defined in the second @overload. In order to work, one always needs to write in last the overload with the broader input type, and the first one with the narrower one!

  2. The output depends on the value of a parameter? Consider using Literal, for example:

def get_nanarray(self, return_mask: bool = False) -> np.ndarray | tuple[np.ndarray, np.ndarray]:

Can be constrained by adding this (Note: for some reason, the default needs to be re-specified in its Literal occurence):

@overload
def get_nanarray(self, return_mask: Literal[False] = False) -> np.ndarray:
    ...

@overload
def get_nanarray(self, return_mask: Literal[True]) -> tuple[np.ndarray, np.ndarray]:
    ...
  1. When the parameter is located after a default argument, use a * to help MyPy look in the right place, and repeat the default function state in one last overload:
@overload
def crop(
    self: RasterType,
    cropGeom: RasterType | Vector | list[float] | tuple[float, ...],
    mode: Literal["match_pixel"] | Literal["match_extent"] = "match_pixel",
    *,
    inplace: Literal[True] = True,
) -> None:
    ...

@overload
def crop(
    self: RasterType,
    cropGeom: RasterType | Vector | list[float] | tuple[float, ...],
    mode: Literal["match_pixel"] | Literal["match_extent"] = "match_pixel",
    *,
    inplace: Literal[False],
) -> RasterType:
    ...

@overload
def crop(
    self: RasterType,
    cropGeom: RasterType | Vector | list[float] | tuple[float, ...],
    mode: Literal["match_pixel"] | Literal["match_extent"] = "match_pixel",
    inplace: bool = True,
) -> RasterType | None:
    ...