43

The python library pathlib provides Path.relative_to. This function works fine if one path is a subpath of the other one, like this:

from pathlib import Path
foo = Path("C:\\foo")
bar = Path("C:\\foo\\bar")
bar.relative_to(foo)

> WindowsPath('bar')

However, if two paths are on the same level, relative_to does not work.

baz = Path("C:\\baz")
foo.relative_to(baz)

> ValueError: 'C:\\foo' does not start with 'C:\\baz'

I would expect the result to be

WindowsPath("..\\baz")

The function os.path.relpath does this correctly:

import os
foo = "C:\\foo"
bar = "C:\\bar"
os.path.relpath(foo, bar)

> '..\\foo'

Is there a way to achieve the functionality of os.path.relpath using pathlib.Path?

JFB
  • 935
  • 2
  • 8
  • 12
  • 6
    Did you ever solve this? I have run into the same problem. I would like to standardize on using pathlib over os.path whenever I can, but this problem has me stumped. – Phil Apr 13 '17 at 14:45
  • 1
    @Phil it appears in this case you're forced to get back to `os.path.relpath` :( ... It seems the `pathlib` module was not thought of as a replacement of `os.path` :(. Or have you found a `pathlib`-only solution? – Ciprian Tomoiagă Nov 08 '18 at 10:31
  • No, I have not found a ``pathlib``-only solution. – Phil Nov 10 '18 at 14:37

3 Answers3

41

The first section solves the OP's problem, though if like me, he really wanted the solution relative to a common root then the second section solves it for him. The third section describes how I originally approached it and is kept for interest sake.

Relative Paths

Recently, as in Python 3.4-6, the os.path module has been extended to accept pathlib.Path objects. In the following case however it does not return a Path object and one is forced to wrap the result.

foo = Path("C:\\foo")
baz = Path("C:\\baz")
Path(os.path.relpath(foo, baz))

> Path("..\\foo")

Common Path

My suspicion is that you're really looking a path relative to a common root. If that is the case the following, from EOL, is more useful

Path(os.path.commonpath([foo, baz]))

> Path('c:/root')

Common Prefix

Before I'd struck upon os.path.commonpath I'd used os.path.comonprefix.

foo = Path("C:\\foo")
baz = Path("C:\\baz")
baz.relative_to(os.path.commonprefix([baz,foo]))

> Path('baz')

But be forewarned you are not supposed to use it in this context (See commonprefix : Yes, that old chestnut)

foo = Path("C:\\route66\\foo")
baz = Path("C:\\route44\\baz")
baz.relative_to(os.path.commonprefix([baz,foo]))

> ...
> ValueError : `c:\\route44\baz` does not start with `C:\\route`

but rather the following one from J. F. Sebastian.

Path(*os.path.commonprefix([foo.parts, baz.parts]))

> Path('c:/root')

... or if you're feeling verbose ...

from itertools import takewhile
Path(*[set(i).pop() for i in (takewhile(lambda x : x[0]==x[1], zip(foo.parts, baz.parts)))])
Community
  • 1
  • 1
Carel
  • 3,289
  • 2
  • 27
  • 44
7

This was bugging me, so here's a pathlib-only version that I think does what os.path.relpath does.

def relpath(path_to, path_from):
    path_to = Path(path_to).resolve()
    path_from = Path(path_from).resolve()
    try:
        for p in (*reversed(path_from.parents), path_from):
            head, tail = p, path_to.relative_to(p)
    except ValueError:  # Stop when the paths diverge.
        pass
    return Path('../' * (len(path_from.parents) - len(head.parents))).joinpath(tail)
Brett Ryland
  • 1,045
  • 1
  • 9
  • 17
  • 1
    Just a nitpick, this will lead to a different result if any of the paths is a symlink. – ypnos Jul 05 '20 at 20:46
2

A recursive version of @Brett_Ryland's relpath for pathlib. I find this to be a tad more readable and it is going to succeed on first try in most cases so it should have similar performance as the original relative_to function:

def relative(target: Path, origin: Path):
    """ return path of target relative to origin """
    try:
        return Path(target).resolve().relative_to(Path(origin).resolve())
    except ValueError as e: # target does not start with origin
        # recursion with origin (eventually origin is root so try will succeed)
        return Path('..').joinpath(relative(target, Path(origin).parent))
Felix B.
  • 905
  • 9
  • 23