Sieve of eratosthenes: My optimized version
Built-in functions
As mentioned by Marc, Since Python 3.8 you can use math.isqrt.  This is faster.  It is also guaranteed to be accurate when the size of the argument exceeds the number of bits of mantissa in a float.
Type coercion
sieve_arr = array('i', [True for _ in range(n+1)])

Your array is declared to hold 'i' values: integers.  But you are trying to storing boolean values in the array.  This necessitates an unnecessary conversion from True and False to 1 and 0.
Underused variable
The value sieve_arr_length is used once.  It effectively holds n + 1.
In fact, sieve_arr_length = n + 1 might be a more efficient way to initialize it, rather than asking the array for its length.  Then instead of recomputing n+1 each time the j loop starts, you could simply use sieve_arr_length.
Special case even numbers
Every even number greater than 2 is not prime.  There is no need to check for these.  You could simply start your i loop at 3, instead of 2, and go up by twos to check only the odd numbers greater than 1.
Starting the j loop at i+i, or i*2 is an optimization, but not enough of one.  By the time you're checking 13, you've already eliminated 3*13, 5*13, 7*13, and 11*13 because you've eliminated multiples of smaller primes.  You can simply start at i*i.
Plus, if you're only checking the odd prime candidates (because you've special-cased the even), you don't need to eliminate 14*13, 16*13, 18*13 and so on, because those are guaranteed to be even. Your step size can be 2*i, and eliminate half of the redundant assignments.
    # Odd prime candidate sieve ...
    for i in range(3, isqrt(sieve_arr_length) + 1, 2):
        if sieve_arr[i]:
            for j in range(i * i, sieve_arr_length, 2 * i):
                sieve_arr[j] = 0;

Memory
The array('i', ...) uses 4 or 8 (depending on architecture) bytes per element in the array.  RootTwo's solution uses a list structure, instead of the array.  This will increase the amount of storage required for the sieve, since a list must store a pointer in each list element.
Using a bytearray instead of an array('i', ...) would reduce your memory usage, down to 1 byte per sieve element, allowing you to maintain a larger sieve.
def ajneufeld_bytearray(n=80):
    &quot;&quot;&quot;Sieve of Erathosthenes&quot;&quot;&quot;

    prime = bytearray((0, 1)) * ((n + 1) // 2)
    prime[1] = 0
    prime[2] = 1
    for i in range(3, isqrt(n) + 1, 2):
        if prime[i]:
            prime[i*i::2*i] = bytes(len(range(i*i, n, 2*i)))

    return prime

You can further pack 8 flags into each byte, dramatically decreasing the size of your sieve even further, but this bit packing would be complex to implement yourself.  Fortunately it has already been done as a nice package: bitarray.
bitarray implementation
The bitarray package has nice bitslice assignment operators.  RootTwo's slice assignment:
sieve_arr[i*i::i] = [False]*len(range(i*i, len(sieve_arr), i))

is much easier with bitarray.  The equivalent statement would be:
sieve_arr[i*i::i] = False

assigning False to each bit in the slice.
from bitarray import bitarray
from math import isqrt

def ajneufeld(n=80):
    &quot;&quot;&quot;Sieve of Erathosthenes&quot;&quot;&quot;

    prime = bitarray(n)
    prime.setall(False)
    prime[3::2] = True

    prime[2] = True
    for candidate in range(3, isqrt(n) + 1, 2):
        if prime[candidate]:
            prime[candidate*candidate::2*candidate] = False

    return prime

Test code:
import matplotlib.pyplot as plt
from timeit import timeit
from sys import getsizeof

if __name__ == '__main__':

    candidates = (theprogrammer, roottwo, ajneufeld_bytearray, ajneufeld_bitarray)
    limits = [10, 30, 100, 300, 1000, 3000, 10_000, 30_000, 100_000, 300_000]

    print(&quot;Memory usage:&quot;)
    for candidate in candidates:
        result = candidate(100_000)
        print(f&quot;  {candidate.__name__:19} {getsizeof(result):6}&quot;)

    fig, ax = plt.subplots()

    for candidate in candidates:
        times = [ timeit(lambda: candidate(limit), number=1) for limit in limits ]
        ax.plot(limits, times, '-+', label=candidate.__name__)

    ax.legend()
    plt.xscale('log')
    plt.yscale('log')
    plt.show()

Output:

Memory usage:
  theprogrammer       400068
  roottwo             800056
  ajneufeld_bytearray 100057
  ajneufeld_bitarray   12564