ZetCode

Python os.readlink Function

Last modified April 11, 2025

This comprehensive guide explores Python's os.readlink function, which reads the target of symbolic links. We'll cover path resolution, error handling, and practical filesystem navigation examples.

Basic Definitions

The os.readlink function returns a string representing the path to which the symbolic link points. It only works on symbolic links.

Key parameters: path (symbolic link to read). Returns the link target path. Raises OSError if path isn't a symbolic link or other errors occur.

Reading a Basic Symbolic Link

The simplest use of os.readlink reads the target of a symbolic link. First we create a link, then read its target.

basic_readlink.py
import os

# Create a symbolic link
target = "original.txt"
link_name = "symlink.txt"

with open(target, "w") as f:
    f.write("Test content")

os.symlink(target, link_name)

# Read the symbolic link
try:
    link_target = os.readlink(link_name)
    print(f"'{link_name}' points to '{link_target}'")
except OSError as e:
    print(f"Error reading link: {e}")

# Clean up
os.remove(link_name)
os.remove(target)

This example creates a file and symbolic link, then reads the link target. The try/except block handles cases where the path isn't a symbolic link.

Note that os.readlink returns the raw link contents, which may be relative or absolute depending on how the link was created.

Handling Relative Path Links

Symbolic links can contain relative paths. os.readlink returns the exact stored path without resolution. This example demonstrates handling.

relative_link.py
import os

# Create directory structure
os.makedirs("dir1/dir2", exist_ok=True)
with open("dir1/file.txt", "w") as f:
    f.write("Test file")

# Create relative symbolic link
os.chdir("dir1/dir2")
os.symlink("../file.txt", "rel_link.txt")

# Read the link
link_path = "rel_link.txt"
try:
    target = os.readlink(link_path)
    print(f"Link points to: {target}")
    
    # Resolve to absolute path
    abs_path = os.path.abspath(target)
    print(f"Absolute path: {abs_path}")
    
    # Verify the target exists
    if os.path.exists(target):
        print("Target exists")
        with open(target) as f:
            print(f.read())
    else:
        print("Target doesn't exist")
except OSError as e:
    print(f"Error: {e}")

# Clean up
os.chdir("../..")
os.remove("dir1/dir2/rel_link.txt")
os.remove("dir1/file.txt")
os.removedirs("dir1/dir2")

This creates a relative symbolic link and shows how to read and resolve it. The link target "../file.txt" is relative to the link's directory.

To use the link target, you often need to convert it to an absolute path using os.path.abspath or os.path.join with the link's directory.

Reading Absolute Path Links

Symbolic links can also contain absolute paths. os.readlink returns the exact stored path, which may be absolute or relative.

absolute_link.py
import os

# Create a file and absolute symlink
target = os.path.abspath("target_file.txt")
link_name = "abs_link.txt"

with open(target, "w") as f:
    f.write("Absolute path test")

os.symlink(target, link_name)

# Read the absolute link
try:
    link_target = os.readlink(link_name)
    print(f"Link points to: {link_target}")
    
    # Compare with resolved path
    resolved = os.path.realpath(link_name)
    print(f"Resolved path: {resolved}")
    
    # Check if they match
    if os.path.abspath(link_target) == resolved:
        print("Link target matches resolved path")
    else:
        print("Warning: Link target differs from resolved path")
        
except OSError as e:
    print(f"Error: {e}")

# Clean up
os.remove(link_name)
os.remove(target)

This creates an absolute symbolic link and compares the raw link target with the fully resolved path using os.path.realpath.

For absolute links, the readlink result should match the resolved path unless intermediate links exist or filesystem changes occurred.

Error Handling

os.readlink raises OSError for various conditions. This example demonstrates comprehensive error handling for different failure scenarios.

error_handling.py
import os
import errno

test_cases = [
    "nonexistent_link",    # Doesn't exist
    "regular_file.txt",    # Not a symlink
    "/proc/self/fd/0",     # Special file (may not be a link)
    ""                     # Empty path
]

# Create a regular file for testing
with open("regular_file.txt", "w") as f:
    f.write("Test")

for path in test_cases:
    print(f"\nTesting: {path}")
    try:
        target = os.readlink(path)
        print(f"Link target: {target}")
    except OSError as e:
        if e.errno == errno.EINVAL:
            print("Not a symbolic link")
        elif e.errno == errno.ENOENT:
            print("Path does not exist")
        elif e.errno == errno.ENOTDIR:
            print("Component of path is not a directory")
        else:
            print(f"Unexpected error: {e}")

# Clean up
os.remove("regular_file.txt")

This tests various error conditions and handles them with specific error messages. Different errno values indicate different failure reasons.

Common errors include EINVAL (not a symlink), ENOENT (doesn't exist), and EACCES (permission denied). Always handle these cases robustly.

Recursive Link Resolution

Symbolic links can point to other links. This example shows how to safely resolve nested links without infinite loops.

recursive_resolution.py
import os

def resolve_link(path, max_depth=10):
    """Resolve a symbolic link recursively with cycle detection."""
    original_path = path
    seen = set()
    
    while max_depth > 0:
        try:
            if os.path.islink(path):
                if path in seen:
                    raise OSError("Detected symbolic link loop")
                seen.add(path)
                path = os.path.join(os.path.dirname(path), os.readlink(path))
            else:
                return path
        except OSError as e:
            print(f"Error resolving {original_path}: {e}")
            return None
        max_depth -= 1
    
    print(f"Maximum resolution depth reached for {original_path}")
    return path

# Create a chain of symbolic links
os.makedirs("links", exist_ok=True)
with open("links/target.txt", "w") as f:
    f.write("Final target")

os.symlink("target.txt", "links/link2.txt")
os.symlink("link2.txt", "links/link1.txt")

# Test resolution
print("Resolved path:", resolve_link("links/link1.txt"))

# Test cycle detection
os.symlink("link1.txt", "links/link2.txt")  # Create cycle
print("Resolved path:", resolve_link("links/link1.txt"))

# Clean up
os.remove("links/link1.txt")
os.remove("links/link2.txt")
os.remove("links/target.txt")
os.removedirs("links")

This implements safe recursive link resolution with cycle detection. The function follows links until finding a non-link or detecting a loop.

For production use, consider using os.path.realpath instead, which handles this automatically with proper system-level resolution.

Cross-Platform Considerations

Symbolic link behavior varies across platforms. This example demonstrates Windows-specific handling and fallback behavior.

cross_platform.py
import os
import sys
import platform

def read_link_safe(path):
    """Cross-platform symbolic link reading with fallbacks."""
    try:
        if platform.system() == "Windows":
            # Windows requires special handling
            if sys.version_info >= (3, 8):
                # Python 3.8+ has proper symlink support
                return os.readlink(path)
            else:
                # Older Python on Windows may need admin privileges
                import ctypes
                kernel32 = ctypes.windll.kernel32
                if not kernel32.CreateSymbolicLinkW:
                    raise OSError("Symbolic links not supported")
                return os.readlink(path)
        else:
            # Unix-like systems
            return os.readlink(path)
    except (OSError, AttributeError) as e:
        print(f"Error reading link: {e}")
        return None

# Test the function
test_link = "test_link"
try:
    os.symlink("target.txt", test_link)
    print("Link target:", read_link_safe(test_link))
except OSError as e:
    print(f"Couldn't create test link: {e}")
finally:
    if os.path.exists(test_link):
        os.remove(test_link)

This shows platform-specific considerations for symbolic links. Windows has different requirements and capabilities than Unix-like systems.

On Windows, symbolic links often require administrator privileges or developer mode. The implementation also varies across Python versions.

Security Considerations

Best Practices

Source References

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.