The problem is that relative paths have to be specially handled by ntpath._readlink_deep, but ntpath.isabs incorrectly classifies r"\broken\link" as an absolute path. It's actually relative to the current device or drive in the access context. Other path libraries get this right, such as pathlib.Path.is_absolute and C++ path::is_absolute. The documented behavior of ntpath.isabs isn't something that we can change (i.e. "begins with a (back)slash after chopping off a potential drive letter"). So ntpath._readlink_deep needs a private implementation of isabs. For example:
This classifies UNC paths as absolute; rooted paths without a drive as relative; and otherwise depends on splitdrive() to get the root path, if any.
Background
The target of a relative symlink gets evaluated against the solid path that's used to access the link. A solid path contains directories and mountpoints, but no symlinks. In particular, a rooted target such as r"\spam\eggs" is relative to the root device of the solid path that's used to access the link. This may or may not be the device on which the link resides. It depends on how it's accessed. For example, if the volume that contains the symlink is accessed via its DOS device name "V:", then it resolves to r"V:\spam\eggs". Similarly, if the symlink is accessed via r"C:\Mount\VolumeSymlink", where "VolumeSymlink" is a directory symlink to "V:\\\\", then it also resolves to r"V:\spam\eggs". On the other hand, if the symlink is accessed via the mount point r"C:\Mount\VolumeMountpoint", then it resolves to r"C:\spam\eggs".
changed the title [-]ntpath.realpath fails for broken symlinks with rooted target paths[/-][+][ntpath] realpath fails for broken symlinks with rooted target paths[/+]on Aug 8, 2022
Fixed in 3.13 by GH-113829, which made ntpath.isabs() return False for a rooted path without a drive, so _readlink_deep() now joins such target with the location of the link.
Activity
eryksun commented on Jun 3, 2020
ntpath.realpath fails to resolve the non-strict path of a broken relative symlink if the target is a rooted path. For example:
>>> os.readlink('symlink') '\\broken\\link' >>> os.path.realpath('symlink') '\\broken\\link' >>> os.path.abspath('symlink') 'C:\\Temp\\symlink'The problem is that relative paths have to be specially handled by ntpath._readlink_deep, but ntpath.isabs incorrectly classifies r"\broken\link" as an absolute path. It's actually relative to the current device or drive in the access context. Other path libraries get this right, such as pathlib.Path.is_absolute and C++ path::is_absolute. The documented behavior of ntpath.isabs isn't something that we can change (i.e. "begins with a (back)slash after chopping off a potential drive letter"). So ntpath._readlink_deep needs a private implementation of isabs. For example:
This classifies UNC paths as absolute; rooted paths without a drive as relative; and otherwise depends on splitdrive() to get the root path, if any.
Background
The target of a relative symlink gets evaluated against the solid path that's used to access the link. A solid path contains directories and mountpoints, but no symlinks. In particular, a rooted target such as r"\spam\eggs" is relative to the root device of the solid path that's used to access the link. This may or may not be the device on which the link resides. It depends on how it's accessed. For example, if the volume that contains the symlink is accessed via its DOS device name "V:", then it resolves to r"V:\spam\eggs". Similarly, if the symlink is accessed via r"C:\Mount\VolumeSymlink", where "VolumeSymlink" is a directory symlink to "V:\\\\", then it also resolves to r"V:\spam\eggs". On the other hand, if the symlink is accessed via the mount point r"C:\Mount\VolumeMountpoint", then it resolves to r"C:\spam\eggs".
serhiy-storchaka commented on Aug 8, 2026
Fixed in 3.13 by GH-113829, which made ntpath.isabs() return False for a rooted path without a drive, so _readlink_deep() now joins such target with the location of the link.
@eryksun, is this the result you expected, and can the issue be closed?