Optimize K-Mean for large number of clusters
First, some Python improvements. You do repeated lookups of the kind cluster_dict[cluster]. If you were to directly iterate over cluster_dict.values() these would all be not needed. Also, list comprehensions are usually faster than building a simple list with a for loop. And finally, docstrings go inside the function:
def update_center(self, cluster_dict):
    &quot;&quot;&quot;Gives new centroids with each new iteration.&quot;&quot;&quot;
    cluster_center = []

    for cluster in cluster_dict.values():
        cluster_list = [self.arr[sample] for sample in cluster]

        # Mean of samples in a given cluster.
        cluster_mean = np.mean(np.array(cluster_list), axis=0).tolist()

        # Find which sample is closest to mean of cluster.
        smallest_distance_from_mean = np.argmin([self.distance(cluster_mean, sample)
                                                 for sample in cluster_list])

        # Build an updated list of cluster centers.
        cluster_center.append(cluster[smallest_distance_from_mean])

    return cluster_center

Another small improvement is not to calculate the sqrt in your distance function, since you only ever need the minimum value, so the squared values work just fine.
def distance(self, arr1, arr2):
    &quot;&quot;&quot;Squared distance between two arrays&quot;&quot;&quot;
    return np.sum(np.subtract(arr1, arr2) ** 2)

However, the greatest improvements are probably to be gained by not using lists everywhere, but to use numpy wherever you can:
class KMeans:
    def __init__(self, arr, k):
        self.arr = np.array(arr)
        self.k = k

    def loop_over_centers(self, max_iter):

        # Choose random centroids.
        chosen_centers = np.random.choice(len(self.arr), self.k, replace=False)

        # Iterate till a certain max number of iterations.
        for _ in range(max_iter):

            cluster_dict = defaultdict(list)

            # Assign samples to cluster centers based on minimum distance
            centers = self.arr[chosen_centers]
            for i, sample in enumerate(self.arr):
                closest_center = np.argmin(np.sum((centers - sample)**2, axis=1))
                cluster_dict[chosen_centers[closest_center]].append(i)
            chosen_centers = self.update_center(cluster_dict)

        return cluster_dict

    def update_center(self, cluster_dict):
        &quot;&quot;&quot;Gives new centroids with each new iteration.&quot;&quot;&quot;
        cluster_center = []
    
        for cluster in cluster_dict.values():
            samples = self.arr[cluster]
            # Mean of samples in a given cluster.
            cluster_mean = np.mean(samples, axis=0)
            closest_sample = np.argmin(np.sum((samples - cluster_mean)**2, axis=1))
            cluster_center.append(cluster[closest_sample])

        return cluster_center

Your code takes 391 ms ± 21.7 ms on my machine, the first two changes drop this down to 340 ms ± 15.9 ms and the final change to 145 µs ± 588 ns, faster by a factor 2700!
You could go even further by doing this part without the for loop using only array operations as well:
        for i, sample in enumerate(self.arr):
            closest_center = np.argmin(np.sum((centers - sample)**2, axis=1))
            cluster_dict[chosen_centers[closest_center]].append(i)

However, this will then take k * len(arr) * dim(arr) memory, which may be a bit too much if both are very large.
Note: You can factor out the distance into a function again, I just don't want to rerun the timings...