Merge sort Python implementation
No, it's not very efficient...  There are operations you typically want to minimize if you want a performing solution, and memory allocation is one of the top ones.  But your merge_arrays function is creating a new list every time it gets called, which is not a good thing.  An efficient implementation of mergesort will do the sorting in-place (with a copy of the list before if the original has to be kept unchanged), and will reuse the merge buffer for all merging operations.

def mergesort(array, left=0, right=None):
    if right is None:
        right = len(array)
    if right - left &lt;= 1:
        return

    mid = left + (right - left) // 2
    mergesort(array, left, mid)
    mergesort(array, mid, right)
    merge(array, left, mid, right)

def merge(array, left, mid, right):
    buffer = array[left:mid]
    read_left = 0
    read_right = mid
    write = left
    while read_left &lt; len(buffer) and read_right &lt; right:
        if array[read_right] &lt; buffer[read_left]:
            array[write] = array[read_right]
            read_right += 1
        else:
            array[write] = buffer[read_left]
            read_left += 1
        write += 1
    while read_left &lt; len(buffer):
        array[write] = buffer[read_left]
        read_left += 1
        write += 1


Notice how the left part is the only one copied out into the buffer, as that is all that is needed.  Notice also the loop structure, which is substantially different from yours.  It is a matter of taste, but the single while loop for the merging, with an extra loop to copy items that could be left in the buffer (notice that any item left on the right side is already in the right place) seems to be preferred in most production implementations I've seen, and I find it more clear, but YMMV.

The above code is creating a new buffer for every merge operation, which makes the code a little simpler.  A possible way of reusing a buffer is to have it be a static variable of merge.  Static variables are a little funny in Python, but it could look something like this:

def merge(array, left, mid, right):
    left_len = mid - left
    if len(merge.buffer) &lt; left_len:
        merge.buffer.extend([None] * (len(buffer) - left_len))
    for write, read in enumerate(range(left, mid)):
        merge.buffer[write] = array[read]
    read_left = 0
    read_right = mid
    write = left
    while read_left &lt; left_len and read_right &lt; right:
        if array[read_right] &lt; buffer[read_left]:
            array[write] = array[read_right]
            read_right += 1
        else:
            array[write] = buffer[read_left]
            read_left += 1
        write += 1
    while read_left &lt; len(buffer):
        array[write] = buffer[read_left]
        read_left += 1
        write += 1
merge.buffer = []


I wouldn't be surprised though if, this being Python, the explicit looping took longer than the previous version.  But this would be preferred in a lower level language.

Another possible optimization is to scan the left half of the array prior to the copying into the buffer, to avoid moving around items that are already in the right place:

def merge(array, left, mid, right):
    while array[left] &lt; array[mid]:
        left += 1
    ...


and the rest of the code would stay the same.