38

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

Georgy
  • 12,464
  • 7
  • 65
  • 73
Parham
  • 3,157
  • 4
  • 31
  • 44
  • 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 Dec 13 '12 at 04:01
  • 1
    check this out http://code.activestate.com/recipes/578116-move-files-with-rename-if-required/ – avasal Dec 13 '12 at 04:03
  • 1
    check [`filename_fix_existing(filename)`](https://github.com/steveeJ/python-wget/blob/master/wget.py#L72) – Grijesh Chauhan Jan 02 '16 at 10:41

17 Answers17

31

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
starikcetin
  • 1,391
  • 1
  • 16
  • 24
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'))
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • 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 – Parham Dec 13 '12 at 09:17
  • 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. – unutbu Dec 13 '12 at 13:22
11

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)
llk
  • 141
  • 2
  • 6
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.

arbulgazar
  • 1,931
  • 1
  • 19
  • 24
8

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
lastro
  • 81
  • 1
  • 3
5

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
Basj
  • 41,386
  • 99
  • 383
  • 673
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)
Community
  • 1
  • 1
jtpereyda
  • 6,987
  • 10
  • 51
  • 80
  • Thanks ! Do you not need to do something special in order to allow it to be used as a context ? – Ciprian Tomoiagă Jul 04 '19 at 14:18
  • 1
    The function just needs to return a file handle -- that's all the built in open() function does anyway. See also https://stackoverflow.com/questions/3774328/implementing-use-of-with-object-as-f-in-custom-class-in-python – jtpereyda Jul 13 '19 at 17:43
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
OnGle
  • 132
  • 6
0

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
penny chan
  • 769
  • 1
  • 10
  • 15
0
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
Attaullah Khan
  • 303
  • 2
  • 8
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.

naspitha
  • 1
  • 1
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
-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!

Chris
  • 310
  • 4
  • 14
-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}"
ibhax
  • 1
  • 1
-2
def create_file():
        counter = 0
        filename = "file"
        while os.path.isfile(f"dir/{filename}{counter}.txt"):
            counter += 1
        print(f"{filename}{counter}.txt")
M1nybe
  • 37
  • 4
-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
Yevhen_Radchenko
  • 965
  • 8
  • 15
  • 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. – Alex Guteniev Feb 26 '19 at 20:19
-3

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

Achak
  • 1