Is there an idiomatic short way to ensure a os.PathLike argument is a path without copy if it already is?

TLDR;

Is there a something like numpy’s asarray for pathlib.Path that just returns the object without conversion/copy in case it’s already a path (as opposed to a str | bytes | os.PathLike or anything else the constructor supports).

Background
To accept objects that represent a path the abstract interface os.PathLike can be used and conveniently enables a function to get a string/bytes representation using os.fspath regardless of the passed in object.

I often however find myself wanting to work with pathlib.Path objects while still allowing paths passed as str | bytes without explicitly first checking for the types (as all I really care about is the resulting type). This can of course be done by always constructing a Path object from the argument but that always results in a copy even if the argument already is a Path which would also be the most common case for functions calling eachother.

Of course the idiom path = path if isinstance(path, pathlib.Path) else pathlib.Path(path) works but the pattern and need feels common enough that I feel that there is a reasonable chance it’s already thought of and I’m just missing it, especially since the path objects are mostly immutable and returns copies on all modifications anyway.

It could of course be that the thought is that the copy for every level in pathlib.Path.__new__ (though the call to _from_parts) is performant enough that the pattern with copy is the intended way.

Edit: Added current example as clarification that I have no problem solving it with a function and the question is more about if the use-case was already catered for in a builtin way and I’m failing to find it.

Current solutions to clarify that it’s not about how to solve the problem

def ensure_path(path: os.PathLike | str | bytes) -> pathlib.Path:
    return path if isinstance(path, pathlib.Path) else pathlib.Path(path)

# This isn't that onerous of course but it feels common enough that I 
# expected it may have some existing solution.
def accept_path_argument(path: path: os.PathLike | str | bytes): 
    path_ = ensure_path(path)

# This is of course also fine with only drawback needless garbage creation
def accept_path_argument(path: path: os.PathLike | str | bytes): 
    path_ = pathlib.Path(path)

1 Like

There is a moderately nice solution where you define a decorator, so that you can use for example

@pathlike_argument("file")
def f(file: Path | str, ...): ...

then you can hide the path = path if isinstance(path, pathlib.Path) else pathlib.Path(path) ugliness inside an imported function.

Personally I don’t ever worry about the memory overhead of creating new Path objects.

1 Like
    match path:
         case Path:
              pass
         case str | bytes:
               path = Path(path)
         case _:
                raise ValueError('path must be a Path, str, or bytes object.')

Would that work for you?

Nice solution even if I probably personally prefer the direct solution without decorator.

It’s not the memory useage as such as they are garbage collected but rather the churn and performance of doing it for something that is immutable anyway.

I’m not sure this improves upon a simple isinstance and call to the constructor. Also this will raise a ValueError for any object implementing os.PathLike objects that aren’t the listed ones even if Path would handle them fine.

Fair enough. You can do

    case _:
        path = Path(path)

assuming you know it is os.PathLike.

Given that Path objects are already documented as immutable

It wouldn’t seem unreasonable for __new__ to just return the passed in object if it’s already an instance of the correct class. Only real difference would be for code relying on object identity which feels very specific and hard to find a reason to do.

That might be a question better posed in the Ideas topic. There are probably reasons beyond what I can explain for why it’s not feasible.