PurePath.relative_to

Both are lexical: they compare path components, never the file system. relative_to strips the other path off the front; with walk_up=True (3.12+) it may climb with ".." like os.path.relpath.

pathlib methodPython 3.4+ (is_relative_to 3.9+, walk_up 3.12+)Live demo
Common call
PurePosixPath('/srv/app/static/logo.svg').relative_to('/srv/app')
Returns
PurePosixPath('static/logo.svg')
Replaces
os.path.relpath (with walk_up=True) and startswith() checks
Watch out
not inside → ValueError: '…' is not in the subpath of '…'
PurePath.relative_to(otherother — The base path.type: str | os.PathLike · required, walk_upwalk_up — relative_to only (3.12+): allow '..' segments when this path is not under other. Keyword-only in practice.type: bool · default: False=False) / .is_relative_to(other)
→ PurePath | bool

Demo

Live evaluation
Strip other off the front of path.
Try:
Inputs
pathstrthe path
otherstrbase path
Code
from pathlib import PurePosixPath
PurePosixPath('/srv/app/static/logo.svg').relative_to('/srv/app')
Result
PurePosixPath('static/logo.svg')

The checks are per component, so '/srv/application' is not under '/srv/app' even though the string starts with it. They are also purely textual: '/srv/app/../secret' counts as inside '/srv/app' because '..' is not resolved — call resolve() first when that matters. With walk_up=True the error changes: an absolute path and a relative one have different anchors, and a '..' inside other cannot be walked.

Parameters

NameTypeRequiredDescription
otherstr | os.PathLikeyesThe base path.
walk_upboolno (False)relative_to only (3.12+): allow '..' segments when this path is not under other. Keyword-only in practice.

Return value

PurePath | bool — relative_to returns a relative path; is_relative_to returns True or False.

Common patterns

Paths relative to a project root
Nice short names for logs and reports.
from pathlib import Path
root = Path.cwd()
for p in sorted(root.rglob('*.py')):
    print(p.relative_to(root).as_posix())
Refuse paths that escape a folder
Resolve both sides first so ".." and symlinks are accounted for.
from pathlib import Path
base = Path('uploads').resolve()
target = (base / name).resolve()
if not target.is_relative_to(base):
    raise PermissionError(name)
Mirror a tree into another folder
relative_to gives the part to re-attach under the destination.
from pathlib import Path
src, dst = Path('src'), Path('build')
for f in src.rglob('*.txt'):
    out = dst / f.relative_to(src)

Examples

1. Strip a base folder
from pathlib import PurePosixPath PurePosixPath('/home/ada/docs/cv.pdf').relative_to('/home/ada')
Returns
PurePosixPath('docs/cv.pdf')
2. Same path gives "."
from pathlib import PurePosixPath PurePosixPath('/home/ada').relative_to('/home/ada')
Returns
PurePosixPath('.')
3. Not inside: ValueError
from pathlib import PurePosixPath PurePosixPath('/etc/passwd').relative_to('/usr')
Returns
ValueError: '/etc/passwd' is not in the subpath of '/usr'
4. walk_up=True climbs with ..
from pathlib import PurePosixPath PurePosixPath('/etc/passwd').relative_to('/usr', walk_up=True)
Returns
PurePosixPath('../etc/passwd')
5. Different anchors
from pathlib import PurePosixPath PurePosixPath('/etc/passwd').relative_to('etc', walk_up=True)
Returns
ValueError: '/etc/passwd' and 'etc' have different anchors
6. is_relative_to never raises
from pathlib import PurePosixPath (PurePosixPath('/srv/app/x').is_relative_to('/srv'), PurePosixPath('/srv/app/x').is_relative_to('/etc'))
Returns
(True, False)
7. Components, not characters
from pathlib import PurePosixPath PurePosixPath('/srv/application').is_relative_to('/srv/app')
Returns
False

Pitfalls

1. Checking containment with str.startswith
A string prefix is not a parent folder: '/srv/app2' starts with '/srv/app'.
startswith
'/srv/app2/secret'.startswith('/srv/app')
True
is_relative_to
from pathlib import PurePosixPath
PurePosixPath('/srv/app2/secret').is_relative_to('/srv/app')
False
2. Trusting a lexical check with ".." in the path
Without resolve(), '..' is just a name, so a path that climbs out still looks inside.
lexical
from pathlib import PurePosixPath
PurePosixPath('/srv/app/../../etc/passwd').is_relative_to('/srv/app')
True
resolve() first
from pathlib import Path
base = Path('app').resolve()
(base / '../../etc/passwd').resolve().is_relative_to(base)
False
3. Expecting relpath behaviour by default
Unlike os.path.relpath, relative_to refuses to add ".." unless you pass walk_up=True (3.12+).
default
from pathlib import PurePosixPath
PurePosixPath('/srv/logs').relative_to('/srv/app')
ValueError: '/srv/logs' is not in the subpath of '/srv/app'
walk_up=True
from pathlib import PurePosixPath
PurePosixPath('/srv/logs').relative_to('/srv/app', walk_up=True)
PurePosixPath('../logs')

When to use

Use it
  • Displaying paths relative to a root folder
  • Mirroring a directory tree under a new base
  • Containment checks (after resolve())
Reach for something else
  • Paths with unresolved ".." or symlinks → resolve() both sides first
  • Paths on different drives (Windows) → no relative path exists; handle the ValueError

Notes

CPython impl
Lib/pathlib/_local.py: walks other and its parents until one equals self or one of self.parents; the number of steps becomes the ".." count
Errors
'X' is not in the subpath of 'Y' (not inside, walk_up=False) · 'X' and 'Y' have different anchors (walk_up=True, e.g. absolute vs relative) · '..' segment in 'Y' cannot be walked
Deprecated
Passing extra positional arguments (joined onto other) is deprecated since 3.12 and removed in 3.14
os.path.relpath
relpath makes both paths absolute against the cwd first; relative_to is purely lexical

FAQ

target.relative_to(base). If target is not inside base, pass walk_up=True (Python 3.12+) to get a path with '..' segments, like os.path.relpath.