Generation of products of power
global

there is no need to make primes a global. You only read from it, but don't assign to it, you can use it as is. It will be even faster if you make primes a local variable by passing it in as a parameter, so Python uses the  LOAD_FAST bytecode instead of LOAD_GLOBAL. Since primes is called, indexed and sliced a lot, this can make a difference.

while condition

i1 &lt; len(primes) and i2 &lt; len(primes) and i3 &lt; len(primes) and .... Since i1&lt;i2&lt;i3, only i3 &lt; len(primes) is needed. If you use len(primes) so often, it pays to make it a local variable.

return, don't print

your method immediately prints the results. In general it is better to split the calculation and presentation, so to let the method return or yield the values, and another method do the presentation

looping

I suggest you watch the talk Loop like a Pro by David Baumgold. It's recommended material for every python programmer. 

Instead of looping over the indices, you can loop over the primes-list immediately.

For p, and the first index (i), you can loop over enumerate(primes)

Then you can use this index i to slice primes to only include the elements with an index larger than p and go on to r, so you arrive at the following, naive implementation:

def generate4_naive(a, b, c, limit):
    for i, p in enumerate(primes):
        for j, q in enumerate(primes[i+1:], i+1):
            for r in primes[j+1:]:
                product = p**a * q**b * r**c
                if product &lt; limit:
                    yield p, q, r, product


If you include the early breaks, you arrive at something like this:

def generate4(a, b, c, limit):
    for i, p in enumerate(primes):
        for j, q in enumerate(primes[i+1:], i+1):
            sub_product = p**a * q**b
            for r in primes[j+1:]:
                product = sub_product * r**c
                if product &gt; limit:
                    break
                yield p, q, r, product
            if sub_product * primes[j+1]**c &gt; limit:
                break
        if p ** a * primes[i+1] **b * primes[i+2] ** c &gt; limit:
            return


then I also used primes as a local variable by changing the signature: def generate4b(a, b, c, limit, primes):

Performance:

limit = 1_000_000
%timeit tuple(generate3(2,3,4, limit=limit))



16.6 µs ± 166 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)



%timeit tuple(generate3b(2,3,4, limit=limit, primes=primes))



16.2 µs ± 138 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)



%timeit tuple(generate4(2,3,4, limit=limit))



14.2 µs ± 643 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)



%timeit tuple(generate4b(2,3,4, limit=limit, primes=primes))



7.72 ms ± 516 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)



%timeit list(generate4_naive(2,3,4, 1_000)) # primes also only to 1000



940 ms ± 84.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)