Select and delete objects from S3 container
I think your function names could use some work. There is no need to specify that return_candidates_for_deletion actually returns those candidates (it is obvious). It is also a bit superfluous to know that extract_list_of_backups returns a list. It is enough to know that it returns a collection that is iterable (whether or not that is a tuple, list, set or even just a generator does not really matter for the rest of the code).

In addition, you can save a lot of code by changing your internal data structure a bit. The main thing your code does is sort the backups by the gitlab version and then keep only some of them. So you should store the files in a dictionary with the gitlab version as the key:

import boto3
from collections import defaultdict

class GitlabBackups:
    def __init__(self, bucket_name):
        self.bucket_name = bucket_name

        self.s3 = boto3.resource('s3')
        self.bucket = self.s3.Bucket(bucket_name)

    def backups(self):
        return (obj.key
                for obj in self.bucket.objects.all()
                if 'gitlab_backup' in obj.key)

    def group_by_version(self, files):
        backups = defaultdict(list)
        for file_name in files:
            gitlab_version = file_name.split('_')[4]
            backups[gitlab_version].append(file_name)
        return backups

    def candidates_for_deletion(self, generations_to_keep=2):
        for files in self.group_by_version(self.backups()).values():
            # files.sort()  # is this not needed?
            yield from files[:-generations_to_keep]

    def delete_old_backups(self, generations_to_keep=2):
        for key in self.candidates_for_deletion(generations_to_keep):
            self.s3.Object(self.bucket_name, key).delete()


Here I put all the related standalone functions into the class. It has now one generator that yields all backup files, one that creates the dictionary with the versions as keys, one generator that yields all files except for the generation to keep for each version and then the actual deleting method.

Note that with this, as with your code, it is not guaranteed that the latest generation is actually at the end of the list of files. It depends on the order of self.bucket.objects.all(), so on how S3 returns it (which is apparently alphabetical). But you might want to make sure from your side as well and sort each files list in candidates_for_deletion.



In order to make this a bit more reusable, you could then pull out a groupby method that is similar to itertools.groupby but different because it consumes the whole input and therefore does not require the input to be sorted:

def groupby(objects, key=lambda x: x):
    out = defaultdict(list)
    for obj in objects:
        out[key(obj)].append(obj)
    return out

class GitlabBackups:
    ...
    @staticmethod
    def gitlab_version(file_name):
        return file_name.split('_')[4]

    def candidates_for_deletion(self, generations_to_keep=2):
        for files in groupby(self.backups(), key=self.gitlab_version).values():
            yield from files[:-generations_to_keep]