'Using mypy `NewType` with type aliases or protocols

I want to define a NewType like this:

from typing import NewType
from os import PathLike

AnyPath = PathLike[str] | str
RepoPath = NewType("RepoPath", AnyPath)          # ERR: Argument 2 to NewType(...) must be subclassable (got "Union[PathLike[str], str]")
# RepoPath = NewType("RepoPath", PathLike[str])  # ERR: NewType cannot be used with protocol classes

Basically so that later I can pass a raw path "str" or a "pathlib.Path" to functions, and they can enforce this is specifically a path to a "Repo" rather than a random path. This is useful because there are a lot of paths and urls etc in my code and I don't want them to get mixed up (I don't want to use (Apps) hungarian notation especially either).

Is there a good way to get the type checker to do this for me?



Solution 1:[1]

Ok, here's a solution without Protocol - i.e. rather than accepting anything defining __fspath__ per os.PathLike, this code only allows concrete pathlib.Path or str.

Basically make two NewTypes then accept a union of them rather than a single NewType which is a union of subtypes.

from typing import NewType, overload, TypeAlias  # py3.10 +
from pathlib import Path
#from os import PathLike  # can't get this to work with NewType

AnyPath: TypeAlias = Path | str

RepoPathP = NewType("RepoPathP", Path)
RepoPathS = NewType("RepoPathS", str)

AnyRepoPath: TypeAlias = RepoPathP | RepoPathS


@overload
def RepoPath(path: str) -> RepoPathS: ...
@overload
def RepoPath(path: Path) -> RepoPathP: ...

def RepoPath(path: AnyPath) -> AnyRepoPath:
    if isinstance(path, str):
        return RepoPathS(path)
    else:
        return RepoPathP(path)


def foo(repo: AnyRepoPath) -> None:
    print(repo)

    
foo("bad")                        # Argument 1 to "foo" has incompatible type "str"; expected "Union[RepoPathP, RepoPathS]"
foo(Path("still bad"))            # Argument 1 to "foo" has incompatible type "Path"; expected "Union[RepoPathP, RepoPathS]"
foo(RepoPath("good"))             # Pass
foo(RepoPath(Path("also good")))  # Pass

As mentioned this isn't perfect, as:

class MyCustomPath():
    def __fspath__(self) -> str:
        return r"C:/my/custom/dir"

path: os.PathLike = MyCustomPath() #fine, as expected
repo = RepoPath(path) #fails, since RepoPath accepts only str|Path, not str|PathLike

where ideally I'd like it to succeed

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Greedo