48

Does Python have any built-in functionality to add a number to a filename if it already exists?

My idea is that it would work the way certain OS's work - if a file is output to a directory where a file of that name already exists, it would append a number or increment it.

I.e: if "file.pdf" exists it will create "file2.pdf", and next time "file3.pdf".

3
  • This would generally be the function of the application or program creating the files so no, there is no native functionality like this. Given the directory and file name you could create something yourself. Commented Dec 13, 2012 at 4:01
  • 1
    check this out code.activestate.com/recipes/… Commented Dec 13, 2012 at 4:03
  • 1
    check filename_fix_existing(filename) Commented Jan 2, 2016 at 10:41

18 Answers 18

43

I ended up writing my own simple function for this. Primitive, but gets the job done:

def uniquify(path):
    filename, extension = os.path.splitext(path)
    counter = 1

    while os.path.exists(path):
        path = filename + " (" + str(counter) + ")" + extension
        counter += 1

    return path
Sign up to request clarification or add additional context in comments.

1 Comment

This is a better solution compact and neat.
15

In a way, Python has this functionality built into the tempfile module. Unfortunately, you have to tap into a private global variable, tempfile._name_sequence. This means that officially, tempfile makes no guarantee that in future versions _name_sequence even exists -- it is an implementation detail. But if you are okay with using it anyway, this shows how you can create uniquely named files of the form file#.pdf in a specified directory such as /tmp:

import tempfile
import itertools as IT
import os

def uniquify(path, sep = ''):
    def name_sequence():
        count = IT.count()
        yield ''
        while True:
            yield '{s}{n:d}'.format(s = sep, n = next(count))
    orig = tempfile._name_sequence 
    with tempfile._once_lock:
        tempfile._name_sequence = name_sequence()
        path = os.path.normpath(path)
        dirname, basename = os.path.split(path)
        filename, ext = os.path.splitext(basename)
        fd, filename = tempfile.mkstemp(dir = dirname, prefix = filename, suffix = ext)
        tempfile._name_sequence = orig
    return filename

print(uniquify('/tmp/file.pdf'))

2 Comments

Thank you for the answer! Tricky stuff to figure out from the docs ;) I am opting for my own, simpler, approach then but this answer clearly answers what I was wondering
Yes, that is probably a wise choice if you do not need the special capabilities of tempfile. The tempfile module takes pains to avoid certain race conditions, security, and denial of service attacks. Using sequential numbering makes the above code vulnerable to a denial of service attack. And I am not entirely sure the above is safe from race conditions or other security risks either.
13

If all files being numbered isn't a problem, and you know beforehand the name of the file to be written, you could simply do:

import os

counter = 0
filename = "file{}.pdf"
while os.path.isfile(filename.format(counter)):
    counter += 1
filename = filename.format(counter)

1 Comment

The most elegant of them all!
9

I was trying to implement the same thing in my project but @unutbu's answer seemed too 'heavy' for my needs so I came up with following code finally:

import os
index = ''
while True:
    try:
        os.makedirs('../hi'+index)
        break
    except WindowsError:
        if index:
            index = '('+str(int(index[1:-1])+1)+')' # Append 1 to number in brackets
        else:
            index = '(1)'
        pass # Go and try create file again

Just in case someone stumbled upon this and requires something simpler.

1 Comment

very useful, I used it with os.path.isfile(fname) logic worked well
7

recently I encountered the same thing and here is my approach:

import os

file_name = "file_name.txt"
if os.path.isfile(file_name):
    expand = 1
    while True:
        expand += 1
        new_file_name = file_name.split(".txt")[0] + str(expand) + ".txt"
        if os.path.isfile(new_file_name):
            continue
        else:
            file_name = new_file_name
            break

Comments

6

Let's say you already have those files:

enter image description here

This function generates the next available non-already-existing filename, by adding a _1, _2, _3, ... suffix before the extension if necessary:

import os

def nextnonexistent(f):
    fnew = f
    root, ext = os.path.splitext(f)
    i = 0
    while os.path.exists(fnew):
        i += 1
        fnew = '%s_%i%s' % (root, i, ext)
    return fnew

print(nextnonexistent('foo.txt'))  # foo_3.txt
print(nextnonexistent('bar.txt'))  # bar_1.txt
print(nextnonexistent('baz.txt'))  # baz.txt

Comments

2

Since the tempfile hack A) is a hack and B) still requires a decent amount of code anyway, I went with a manual implementation. You basically need:

  1. A way to Safely create a file if and only if it does not exist (this is what the tempfile hack affords us).
  2. A generator for filenames.
  3. A wrapping function to hide the mess.

I defined a safe_open that can be used just like open:

def iter_incrementing_file_names(path):
    """
    Iterate incrementing file names. Start with path and add " (n)" before the
    extension, where n starts at 1 and increases.

    :param path: Some path
    :return: An iterator.
    """
    yield path
    prefix, ext = os.path.splitext(path)
    for i in itertools.count(start=1, step=1):
        yield prefix + ' ({0})'.format(i) + ext


def safe_open(path, mode):
    """
    Open path, but if it already exists, add " (n)" before the extension,
    where n is the first number found such that the file does not already
    exist.

    Returns an open file handle. Make sure to close!

    :param path: Some file name.

    :return: Open file handle... be sure to close!
    """
    flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY

    if 'b' in mode and platform.system() == 'Windows':
        flags |= os.O_BINARY

    for filename in iter_incrementing_file_names(path):
        try:
            file_handle = os.open(filename, flags)
        except OSError as e:
            if e.errno == errno.EEXIST:
                pass
            else:
                raise
        else:
            return os.fdopen(file_handle, mode)

# Example
with safe_open("some_file.txt", "w") as fh:
    print("Hello", file=fh)

2 Comments

Thanks ! Do you not need to do something special in order to allow it to be used as a context ?
The function just needs to return a file handle -- that's all the built in open() function does anyway. See also stackoverflow.com/questions/3774328/…
0

I haven't tested this yet but it should work, iterating over possible filenames until the file in question does not exist at which point it breaks.

def increment_filename(fn):
    fn, extension = os.path.splitext(path)

    n = 1
    yield fn + extension
    for n in itertools.count(start=1, step=1)
        yield '%s%d.%s' % (fn, n, extension)

for filename in increment_filename(original_filename):
    if not os.isfile(filename):
        break

Comments

0

I found that the os.path.exists() conditional function did what I needed. I'm using a dictionary-to-csv saving as an example, but the same logic could work for any file type:

import os 

def smart_save(filename, dict):
    od = filename + '_' # added underscore before number for clarity

    for i in np.arange(0,500,1): # I set an arbitrary upper limit of 500
        d = od + str(i)

        if os.path.exists(d + '.csv'):
            pass

        else:
            with open(d + '.csv', 'w') as f: #or any saving operation you need
                for key in dict.keys():
                    f.write("%s,%s\n"%(key, dictionary[key]))
            break

Note: this appends a number (starting at 0) to the file name by default, but it's easy to shift that around.

Comments

0

This function validates if the file name exists using regex expresion and recursion

def validate_outfile_name(input_path):
    filename, extension = os.path.splitext(input_path)
    if os.path.exists(input_path):
        output_path = ""
        pattern = '\([0-9]\)'
        match = re.search(pattern, filename)
        if match:
            version = filename[match.start() + 1]
            try: new_version = int(version) + 1 
            except: new_version = 1
            output_path = f"{filename[:match.start()]}({new_version}){extension}"
            output_path = validate_outfile_name(output_path)
        else: 
            version = 1
            output_path = f"{filename}({version}){extension}"
    
        return output_path
    else:
        return input_path

Comments

0

Here is an answer that does not contain a race condition from checking if the file exists in advance. It caps out at 999 attempts so the program won't hang forever if the file fails to create, the same as a shell usually would.

import os


def create_file_unique(path, read=False, binary=False, min_=1, max_=1000):
  mode = ''.join(('x', '+' if read else '', 'b' if binary else ''))
  
  def increment_file_name():
    root, ext = os.path.splitext(path)
    
    return (path if num == min_ else '%s (%d)%s' % (root, num, ext
      ) for num in range(min_, max_))
  
  for i in increment_file_name():
    try:
      return open(i, mode)
    except FileExistsError:
      pass
  
  raise IOError('The file %r could not be created uniquely.' % path)


if __name__ == '__main__':
  with create_file_unique('unique.txt') as f:
    print('Hello World', file=f)

By default, it will open the file with write-only permissions in text mode. Specify read as True for read and write permissions, and binary as True for binary mode. (Note that having read-only permissions would be useless - after all, you just created the file blank!)

Be aware that for the sake of simplicity, this answer (as well as the other answers here) assumes the filename passed in is "bare." That is to say, it will not handle the scenario where the filename passed in already contains a number in parenthesis, like create_file_unique('unique (2).txt'). In that case, this function will add another number on the end, resulting in unique (2) (2).txt. This differs from the typical behaviour of OS functions intended to accomplish this task, like PathYetAnotherMakeUniqueName from the Windows shell, which will check if the filename passed in already contains a number in parenthesis and will update that number in place if it finds one.

If you're willing to forego sequential filenames, you could use NamedTemporaryFile with a prefix, suffix, and deletion turned off, in order to accomplish something similar.

import os
from tempfile import NamedTemporaryFile


def create_file_unique(path, mode='w+b'):
  root, ext = os.path.splitext(path)
  
  return NamedTemporaryFile(
    delete=False, mode=mode,
    prefix=root, suffix=ext, dir=''
  )

Comments

-1

This works for me. The initial file name is 0.yml, if it exists, it will add one until meet the requirement

import os
import itertools

def increment_filename(file_name):
    fid, extension = os.path.splitext(file_name)

    yield fid + extension
    for n in itertools.count(start=1, step=1):
        new_id = int(fid) + n
        yield "%s%s" % (new_id, extension)


def get_file_path():
    target_file_path = None
    for file_name in increment_filename("0.yml"):
        file_path = os.path.join('/tmp', file_name)
        if not os.path.isfile(file_path):
            target_file_path = file_path
            break
    return target_file_path

Comments

-1
import os

class Renamer():
    def __init__(self, name):
        self.extension = name.split('.')[-1]
        self.name = name[:-len(self.extension)-1]
        self.filename = self.name
    def rename(self):
        i = 1
        if os.path.exists(self.filename+'.'+self.extension):
            while os.path.exists(self.filename+'.'+self.extension):
                self.filename = '{} ({})'.format(self.name,i)
                i += 1
        return self.filename+'.'+self.extension

Comments

-1

I've implemented a similar solution with pathlib:

Create file-names that match the pattern path/<file-name>-\d\d.ext. Perhaps this solution can help...

import pathlib
from toolz import itertoolz as itz

def file_exists_add_number(path_file_name, digits=2):

    pfn = pathlib.Path(path_file_name)
    parent = pfn.parent     # parent-dir of file
    stem = pfn.stem         # file-name w/o extension
    suffix = pfn.suffix     # NOTE: extension starts with '.' (dot)!

    try:
        # search for files ending with '-\d\d.ext'
        last_file = itz.last(parent.glob(f"{stem}-{digits * '?'}{suffix}"))
    except:
        curr_no = 1
    else:
        curr_no = int(last_file.stem[-digits:]) + 1

    # int to string and add leading zeros
    curr_no = str(last_no).zfill(digits)
    path_file_name = parent / f"{stem}-{curr_no}{suffix}"

    return str(path_file_name)

Pls note: That solution starts at 01 and will only find file-pattern containing -\d\d!

Comments

-1
def generate_filename(filepath, name="image", ext="jpg"):
    files_list = os.listdir(filepath)
    file_filter = list(filter(lambda x: x.startswith(name), files_list))
    if files_list and file_filter:
        latest_file = sorted(file_filter, key=lambda x: os.path.splitext(x)[0].split("_")[-1])[-1]
        if latest_file:
            filename, ext = os.path.splitext(latest_file)
            file, num = filename.rsplit("_", 1)
            try:
                new_filename = f"{file}_{str(int(num) + 1)}{ext}"
                return new_filename
            except ValueError:
                return f"{name}_0{ext}"
    else:
        return f"{name}_0.{ext}"

1 Comment

If you wish to make quality contributions to the community, review How to Answer
-2

Easy way for create new file if this name in your folder

if 'sample.xlsx' in os.listdir('testdir/'):

    i = 2
    
    while os.path.exists(f'testdir/sample ({i}).xlsx'):
        i += 1
    
    wb.save(filename=f"testdir/sample ({i}).xlsx")
else:
    wb.save(filename=f"testdir/sample.xlsx")

Comments

-2
def create_file():
        counter = 0
        filename = "file"
        while os.path.isfile(f"dir/{filename}{counter}.txt"):
            counter += 1
        print(f"{filename}{counter}.txt")

1 Comment

If you wish to make quality contributions to the community, review How to Answer
-3

A little bit later but there is still something like this should work properly, mb it will be useful for someone.

You can use built-in iterator to do this ( image downloader as example for you ):

def image_downloader():

        image_url = 'some_image_url'

        for count in range(10):
            image_data = requests.get(image_url).content

            with open(f'image_{count}.jpg', 'wb') as handler:
                handler.write(image_data)

Files will increment properly. Result is:

image.jpg
image_0.jpg
image_1.jpg
image_2.jpg
image_3.jpg
image_4.jpg
image_5.jpg
image_6.jpg
image_7.jpg
image_8.jpg
image_9.jpg

1 Comment

It does not seem to answer original question. The question is about handling collision with pre-existing file, not how to create some file names not colliding each other.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.