Search code examples
pythonpython-3.xpathlib

How to get the relative path between two absolute paths in Python using pathlib?


In Python 3, I defined two paths using pathlib, say:

from pathlib import Path

origin = Path('middle-earth/gondor/minas-tirith/castle').resolve()
destination = Path('middle-earth/gondor/osgiliath/tower').resolve()

How can I get the relative path that leads from origin to destination? In this example, I'd like a function that returns ../../osgiliath/tower or something equivalent.

Ideally, I'd have a function relative_path that always satisfies

origin.joinpath(
    relative_path(origin, destination)
).resolve() == destination.resolve()

Note that Path.relative_to is insufficient in this case since origin is not a destination's parent. Also, I'm not working with symlinks, so it's safe to assume there are none if this simplifies the problem.

How can relative_path be implemented?


Solution

  • In Python 3.12+, Path.relative_to takes a parameter called walk_up which provides exactly this functionality:

    from pathlib import Path
    
    origin = Path('middle-earth/gondor/minas-tirith/castle')
    destination = Path('middle-earth/gondor/osgiliath/tower')
    
    assert destination.relative_to(origin, walk_up=True) == Path("../../osgiliath/tower")
    

    According to the official documentation for PurePath.relative_to(other, walk_up=False):

    When walk_up is False (the default), the path must start with other. When the argument is True, .. entries may be added to form the relative path. In all other cases, such as the paths referencing different drives, ValueError is raised.

    For older Python versions (3.11 and older), os.path.relpath can be used instead:

    import os.path
    from pathlib import Path
    
    origin      = Path('middle-earth/gondor/minas-tirith/castle').resolve()
    destination = Path('middle-earth/gondor/osgiliath/tower').resolve()
    
    assert os.path.relpath(destination, start=origin) == '..\\..\\osgiliath\\tower'
    

    Note that the value returned from os.path.relpath is a string, not a pathlib.Path instance.