Mypy overload error despite matching types

reproducible code:

from pathlib import Path
from typing import overload

from typing_extensions import Literal, TypeAlias

StrPath: TypeAlias = str | Path
OpenModeA: TypeAlias = Literal["r", "w"]
OpenModeB: TypeAlias = Literal["r", "w", "x", "a"]


class Something:
    @overload
    def __init__(self, file: StrPath, mode: OpenModeA = "r") -> None: ...

    @overload
    def __init__(self, file: StrPath, mode: OpenModeB = "r") -> None: ...

    def __init__(self, file: StrPath, mode: OpenModeA | OpenModeB = "r") -> None:
        pass


file = "fileA.txt"

if file == "fileA.txt":
    mode = "r"
elif file == "fileB.txt":
    mode = "w"
else:
    mode = "r"

Something(file=file, mode=mode) # mypy error: No overload variant of "Something" matches argument types "str", "str"Mypycall-overload

env:

❯ poetry env info

Virtualenv
Python:         3.11.8
Implementation: CPython
Path:           .venv
Executable:     .venv\Scripts\python.exe
Valid:          True

mypy:

❯ poetry show mypy
 name         : mypy
 version      : 1.9.0
 description  : Optional static typing for Python

dependencies
 - mypy-extensions >=1.0.0
 - tomli >=1.1.0
 - typing-extensions >=4.1.0

You need to annotate your mode variable. mypy only performs bidirectional inference in the cases where it would be most annoying if it didn’t do it, this is already a pretty complex case, mypy will infer literals if they’re used directly, but not when you use an intermediary variable to store them.

If you do reveal_type(mode) you’ll see that the mode is str, but none of your overloads accept str for mode.

It’s also quite common to add a fallback overload for literal parameters that uses the corresponding non-literal type, if you value user-experience over absolute type safety.

1 Like

Annotatiing an intermediary variable everytime sounds like a pain.

The other solution is adding another overload like so?:

@overload
    def __init__(self, file: StrPath, mode: str = "r") -> None: ...

Yes, although you’d probably avoid providing a default value, so the overload doesn’t overlap when the argument is omitted, so like this:

    @overload
    def __init__(self, file: StrPath, mode: str) -> None: ...
1 Like

Thank you!