22
votes

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".

15
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. - timc

15 Answers

14
votes

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'))
10
votes

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.

10
votes

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
7
votes

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
6
votes

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)
3
votes

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)
1
votes

Let's say I already have those files:

enter image description here

This function generates the next available non-already-existing filename, by adding a _1, _2, _3, etc. 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
0
votes

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
0
votes

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
0
votes
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
0
votes

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!

0
votes

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.

0
votes

As of Python 3.6 [f-strings format appeared]

A great solution is to use a function that takes 2 arguments and returns the first non-existed filename available. On the first iteration, the function tries to check if filename without any number exists. If this filename is already taken, the function tries to apply a number 0 right before the file extension. If this filename is already taken too, the function adds +1 to this number until non-existed filename will be found.

import os

def unique_filename(output_filename, file_extension):
    n = ''
    while os.path.exists(f'{output_filename}{n}{file_extension}'):
        if isinstance(n, str):
            n = -1
        n += 1
    return f'{output_filename}{n}{file_extension}'

For older Python versions you can use [%-strings format]

import os

def unique_filename(output_filename, file_extension):
    n = ''
    while os.path.exists('%s%s%s' % (output_filename, n, file_extension)):
        if isinstance(n, str):
            n = -1
        n += 1
    return '%s%s%s' % (output_filename, n, file_extension)

Another option is a function that takes file_path as an input argument.

file_path will be split into file path with filename and file extension (f.e. ".jpg")

import os

def unique_filename_path(file_path):
    output_filename, file_extension = os.path.splitext(file_path)
    n = ''
    while os.path.exists(f'{output_filename}{n}{file_extension}'):
      if isinstance(n, str):
          n = -1
      n += 1
    return f'{output_filename}{n}{file_extension}'
0
votes

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")

-1
votes

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