53

How do I change a symlink to point from one file to another in Python?

The os.symlink function only seems to work to create new symlinks.

Tom Hale
  • 40,825
  • 36
  • 187
  • 242
meteoritepanama
  • 6,092
  • 14
  • 42
  • 55

8 Answers8

39

If you need an atomic modification, unlinking won't work.

A better solution would be to create a new temporary symlink, and then rename it over the existing one:

os.symlink(target, tmpLink)
os.rename(tmpLink, linkName)

You can check to make sure it was updated correctly too:

if os.path.realpath(linkName) == target:
    # Symlink was updated

According to the documentation for os.rename though, there may be no way to atomically change a symlink in Windows. In that case, you would just delete and re-create.

lellimecnar
  • 399
  • 3
  • 2
  • 12
    Python 3.3 has `os.replace` which can be used instead of `os.rename` to provide the same behaviour on Posix & Windows systems. – Tom Jun 02 '17 at 10:12
  • Won't work on Linux systems - needs `replace`. See [this answer](https://stackoverflow.com/a/55742015/5353461) for a complete solution (including generating a temporary symlink pathname). – Tom Hale Apr 18 '19 at 08:31
  • 1
    @TomHale, this works fine on my Linux system provided the original file you're overriding is also a symlink (true for me). And unlike your solution it works on Python 2.7. – DavidK May 01 '19 at 23:24
  • Note that 1) `replace` raises `IsADirectoryError` if `linkName` is a directory and `tmpLink` will then need to be deleted. 2) `replace` may fail if the two files are not on the same filesystem. I deal with both cases in [this answer](https://stackoverflow.com/a/55742015/5353461). – Tom Hale May 13 '19 at 15:05
34

A little function for Python2 which tries to symlink and if it fails because of an existing file, it removes it and links again.

import os, errno

def symlink_force(target, link_name):
    try:
        os.symlink(target, link_name)
    except OSError, e:
        if e.errno == errno.EEXIST:
            os.remove(link_name)
            os.symlink(target, link_name)
        else:
            raise e

For python3 except condition should be except OSError as e:

Robert Siemer
  • 32,405
  • 11
  • 84
  • 94
  • Has a race condition: the known `link_name` could be created again between removal and symlink creation. Try [atomically overwriting the existing symlink](https://stackoverflow.com/a/55742015/5353461). – Tom Hale Apr 18 '19 at 08:32
  • @TomHale You are right that my solution can fail to do the job. Funny is that your solution can fail, too. ;-) – Robert Siemer Apr 18 '19 at 17:29
  • While my answer did point out its race condition, you're right that my comment didn't highlight it. I've now fixed another race in my answer. There is still the *very* unlikely case that a file is created by another process at a randomly generated pathname. Thanks for your comment on my answer, too. – Tom Hale Apr 19 '19 at 07:16
  • 1
    Would you consider updating to use `except FileExistsError:`? I got a syntax error as written. – Tom Hale Apr 19 '19 at 07:18
  • 1
    Thanks; with "except OSError as e" it worked for me on RPi Python 3.7.3. – peets Dec 16 '19 at 09:42
9

Given overwrite=True, this function will safely overwrite an existing file with a symlink.

It is cognisant of race conditions, which is why it is not short, but it is safe.

import os, tempfile

def symlink(target, link_name, overwrite=False):
    '''
    Create a symbolic link named link_name pointing to target.
    If link_name exists then FileExistsError is raised, unless overwrite=True.
    When trying to overwrite a directory, IsADirectoryError is raised.
    '''

    if not overwrite:
        os.symlink(target, link_name)
        return

    # os.replace() may fail if files are on different filesystems
    link_dir = os.path.dirname(link_name)

    # Create link to target with temporary filename
    while True:
        temp_link_name = tempfile.mktemp(dir=link_dir)

        # os.* functions mimic as closely as possible system functions
        # The POSIX symlink() returns EEXIST if link_name already exists
        # https://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html
        try:
            os.symlink(target, temp_link_name)
            break
        except FileExistsError:
            pass

    # Replace link_name with temp_link_name
    try:
        # Pre-empt os.replace on a directory with a nicer message
        if not os.path.islink(link_name) and os.path.isdir(link_name):
            raise IsADirectoryError(f"Cannot symlink over existing directory: '{link_name}'")
        os.replace(temp_link_name, link_name)
    except:
        if os.path.islink(temp_link_name):
            os.remove(temp_link_name)
        raise

Notes for pedants:

  1. If the function fails (e.g. computer crashes), an additional random link to the target might exist.

  2. An unlikely race condition still remains: the symlink created at the randomly-named temp_link_name could be modified by another process before replacing link_name.

I raised a python issue to highlight the issues of os.symlink() requiring the target to not exist, where I was advised to raise my suggestion on the python-ideas mailing list

Credit to Robert Siemer’s input.

Jeff Vier
  • 3
  • 2
Tom Hale
  • 40,825
  • 36
  • 187
  • 242
  • If something “pathological” happens it is better to cycle without sleep, I would argue. Either that or raise an exception (after X iterations)... Anyway... super theoretical... this is why my solution tried only twice. – Robert Siemer Apr 19 '19 at 17:16
  • @RobertSiemer Thanks for the excellent edits overall. I restored my text about the possible race condition as I believe it is still valid. Please comment if you disagree. – Tom Hale Apr 20 '19 at 06:14
  • You love using those words “race condition”, don’t you? ;-) An “attacking program” with the same permissions could just delete the link after it got properly created (function returned), but before the function caller can do something else. There is nothing to prevent that. I don’t see anything special in case “2.”. It is just another case of “1.”... I used different wording, didn’t I? The function might not just be forcefully “interrupted”; it can also _fail_ for other reasons which would not be “interruptions”. – Robert Siemer Apr 20 '19 at 10:57
  • ...so if no one interferes, this function will work. – So will my solution. The only advantage of replace() is this guarantee: link_name will either be what is was before, or it will be a link to target with no point in between. – Robert Siemer Apr 20 '19 at 11:07
  • Won't work for us poor people still on Python 2.7 where "os.replace()" doesn't exist – DavidK May 01 '19 at 23:20
  • What is the while loop for? Under what conditions would it need to repeat? – vpzomtrrfrt Sep 28 '19 at 04:40
  • @vpzomtrrfrt The loop repeats in the unlikely event that a file is created having the same name as the generated temporary filename, and before the `symlink()`. – Tom Hale Sep 28 '19 at 07:40
  • After all this bragging about race conditions, ironically, your code has a race condition in the function `tempfile.mktemp()` which [has been deprecated](https://docs.python.org/3/library/tempfile.html#deprecated-functions-and-variables) since Python 2.3 and should be no longer used. – Jeyekomon Jan 20 '20 at 16:32
  • @Jeyekomon there is no race from `mktemp`: any other process creating the file between `mktemp` and `symlink` is handled by `FileExistsError`. The link you post includes the word 'may'. I avoid `mkstemp` as it both creates and opens the file, requiring both closing then deleting the file, which must not exist (prerequisite for a successful `symlink`.) – Tom Hale Jan 22 '20 at 09:37
9

You could os.unlink() it first, and then re-create using os.symlink() to point to the new target.

NPE
  • 486,780
  • 108
  • 951
  • 1,012
  • 1
    Has a race condition: the known `link_name` could be created again between removal and symlink creation. Try [atomically overwriting the existing symlink](https://stackoverflow.com/a/55742015/5353461). – Tom Hale Apr 18 '19 at 08:33
8

I researched this question recently, and found out that the best way is indeed to unlink and then symlink. But if you need just to fix broken links, for example with auto-replace, then you can do os.readlink:

for f in os.listdir(dir):
    path = os.path.join(dir, f)
    old_link = os.readlink(path)
    new_link = old_link.replace(before, after)
    os.unlink(path)
    os.symlink(new_link, path)
Russ
  • 10,835
  • 12
  • 42
  • 57
culebrón
  • 34,265
  • 20
  • 72
  • 110
3

Don't forget to add a raise command in the case when e.errno != errno.EEXIST You don't want to hide an error then:

if e.errno == errno.EEXIST:
     os.remove(link_name)
     os.symlink(target, link_name)
else:
    raise
crivera
  • 41
  • 1
0

A quick and easy solution:

while True:
     try:
         os.symlink(target, link_name)
         break
     except FileExistsError:
         os.remove(link_name)

However this has a race condition when replacing a symlink which should always exist, eg:

 /lib/critical.so -> /lib/critical.so.1.2

When upgrading by:

 my_symlink('/lib/critical.so.2.0', '/lib/critical.so')

There is a point in time when /lib/critical.so doesn't exist.

This answer avoids the race condition.

Tom Hale
  • 40,825
  • 36
  • 187
  • 242
0

I like this version more

import os
def force_symlink(src, dst):
    if os.path.exists(dst):
        if os.path.realpath(src) == dst:
            return
        os.unlink(dst)
    os.symlink(src, dst)
Mikhail
  • 7,749
  • 11
  • 62
  • 136