Just a couple things: sieve_list = list(range(end+1)) You don't actually need your list to be `[0, 1, 2, ... ]`. You just need an indicator of whether or not it's true or false. So it's simpler just to start with: sieve_list = [True] * (end + 1) That'll likely perform better as well. When you're iterating over multiples of primes, you're using: range(each_number*2, end+1, each_number) But we can do better than `each_number*2`, we can start at `each_number*each_number`. Every multiple of that prime between it and its square will already have been marked composite (because it will have a factor smaller than `each_number`). That'll save a steadily larger increment of time each iteration. As an optimization, we know up front that 2 and 3 are primes. So we can start our iteration at `5` and ensure that we only consider `each_number` to not be multiples of 2 or 3. That is, alternate incrementing by 4 and 2. We can write this function: def candidate_range(n): cur = 5 incr = 2 while cur < n+1: yield cur cur += incr incr ^= 6 # or incr = 6-incr, or however Full solution: def sieve(end): prime_list = [2, 3] sieve_list = [True] * (end+1) for each_number in candidate_range(end): if sieve_list[each_number]: prime_list.append(each_number) for multiple in range(each_number*each_number, end+1, each_number): sieve_list[multiple] = False return prime_list Impact of various changes with `end` at 1 million, run 10 times: initial solution 6.34s [True] * n 3.64s Square over double 3.01s candidate_range 2.46s Also, I would consider `every_multiple_of_the_prime` as an unnecessary long variable name, but YMMV.