Search code examples
pythontype-hintingmypy

Argument 2 to "join" has incompatible type "Optional[str]"; expected "str"


I'm running mypy pre commit hook to check for any possible type issues and it's keep giving me this error Argument 2 to "join" has incompatible type "Optional[str]"; expected "str" for the code below:

else:
    renamed_paths_dict: CustomConnectorRenameDict = {
         "old_path": os.path.join(
                self.temp_dir, change["file_path"]
           ),
          "new_path": os.path.join(
                self.temp_dir,
                change["new_file_path"], -> this is the line mypy is talking about
           ),
    }

change["new_file_path"] can be either a string or None but in this specific else block, it'll be never None.

How can I fix this issue?

Thanks


Solution

  • Allow me to rewrite your question in such a way that gives a proper minimal reproducible example, throws out all the irrelevant things (unrelated to the actual problem) and keeps only the essentials.

    Question

    If the values in a dictionary are of the type str | None, but I know for certain that one of them is definitely a str (not None), how can I tell a static type checker? The following code produces an error with mypy:

    import os
    
    
    temp_dir = "tmp"
    
    paths: dict[str, str | None] = {}
    ...
    paths["new_file_path"] = "foo"
    ...
    new_path = os.path.join(temp_dir, paths["new_file_path"])
    

    The error:

    Argument 2 to "join" has incompatible type "Optional[str]"; expected "str"  [arg-type]
    

    Answer

    You tell the type checker to expect the value corresponding to the key "new_file_path" to be a str:

    ...
    paths["new_file_path"] = "foo"
    ...
    assert paths["new_file_path"] is not None
    new_path = os.path.join(temp_dir, paths["new_file_path"])
    

    Alternatively:

    ...
    assert isinstance(paths["new_file_path"], str)
    new_path = os.path.join(temp_dir, paths["new_file_path"])
    

    If you don't want to write that extra type guard, you can always use a type: ignore, but you should always try and make those as narrow as possible by using the correct error code to silence:

    new_path = os.path.join(temp_dir, paths["new_file_path"])  # type: ignore[arg-type]
    

    But I would not go that route. The assertion has the added benefit of also giving you a clean and immediately obvious error, if you make a mistake somewhere and the new_file_path value happens to be None.

    I would also absolutely not go the route of short-circuiting with paths["new_file_path"] or "some string". This is even more dangerous because it may introduce silent bugs into your code since you said that you expect the new_file_path value to be a string. If you make a mistake, the code would give you a path to tmp/some string without raising an error.


    PS

    Thanks to @SUTerliakov for pointing out that assertions about specific dictionary values are not entirely safe. If you want to be really precise and safe, you should use an intermediary variable for this:

    ...
    new_file_path = paths["new_file_path"]
    assert new_file_path is not None  # isinstance(new_file_path, str)
    new_path = os.path.join(temp_dir, new_file_path)
    

    For the sake of completeness, you could also use typing.cast like this:

    from typing import cast
    ...
    new_path = os.path.join(temp_dir, cast(str, paths["new_file_path"]))
    

    But this has essentially the same effect as a well placed and specific type: ignore, so I would still recommend the assert.