Title: Why is one of my almost identical algorithms works ~10 times faster then other?
Tags: python-3.x,algorithm

Question: I am new in programming, so i don't understand some more deeper things. I have a task, to create amount of passwords, with a given numb and longennes of passwords. My first code:
from random import choices, choice


def generate_password(m):
    ch = choices('23456789qwertyupasdfghjkzxcvbnmiLQWERTYUPASDFGHJKZXCVBNM', k=m - 3)
    ch.append(choice('LQWERTYUPASDFGHJKZXCVBNM'))
    ch.append(choice('qwertyupasdfghjkzxcvbnmi'))
    ch.append(choice('23456789'))
    return ''.join(ch)


def main(n, m):
    p = set()
    t = 0
    while t < n:
        s = generate_password(m)
        if s not in p:
            t += 1
            p.add(s)
    return p

was working, but too long, so then i tried other ways, randomly improving parts of code. So, why is this code so fast? Is it easier to shuffle() several times, then just use choice() ?
from random import choices, shuffle, choice


def generate_password(m):
    global p
    ch = choices('LQWERTYUPASDFGHJKZXCVBNM', k=m - 3)
    ch.append(choice('LQWERTYUPASDFGHJKZXCVBNM'))
    ch.append(choice('qwertyupasdfghjkzxcvbnmi'))
    ch.append(choice('23456789'))
    while True:
        tt = ''.join(ch)
        if tt in p:
            shuffle(ch)
            continue
        return tt


p = set()


def main(n, m):
    global p
    t = 0
    while t < n:
        p.add(generate_password(m))
        t += 1
    return p

from time import time
from random import choices, shuffle, choice


def generate_password(m):
    global p
    ch = choices('LQWERTYUPASDFGHJKZXCVBNM', k=m - 3)
    ch.append(choice('LQWERTYUPASDFGHJKZXCVBNM'))
    ch.append(choice('qwertyupasdfghjkzxcvbnmi'))
    ch.append(choice('23456789'))
    while True:
        tt = ''.join(ch)
        if tt in p:
            shuffle(ch)
            continue
        return tt


p = set()


def main(n, m):
    global p
    t = 0
    while t < n:
        p.add(generate_password(m))
        t += 1
    return p





t1 = time()
print(*main(4609, 3))
t2 = time()
print(t2 - t1)

output: 0.05657219886779785
from time import time
from random import choices, choice


def generate_password(m):
    ch = choices('23456789qwertyupasdfghjkzxcvbnmiLQWERTYUPASDFGHJKZXCVBNM', k=m - 3)
    ch.append(choice('LQWERTYUPASDFGHJKZXCVBNM'))
    ch.append(choice('qwertyupasdfghjkzxcvbnmi'))
    ch.append(choice('23456789'))
    return ''.join(ch)


def main(n, m):
    p = set()
    t = 0
    while t < n:
        s = generate_password(m)
        if s not in p:
            t += 1
            p.add(s)
    return p





t1 = time()
print(*main(4609, 3))
t2 = time()
print(t2 - t1)

I didn't wait

Answer: In the first case, you generate a random password with m-2 uppercase letters, one lowercase letter and a digit between 2 and 9, after which it gets added to the set if it is not a duplicate. When you start accumulating passwords, the chances of a duplicate increase, so multiple passwords have to be generated before you get a unique one, which is the slowdown you mention.
Now compare that to the second case, where if the password is a duplicare, you shuffle the characters until it is no longer a duplicate. Now the order of m-2 capitals, one lowercase and a digit no longer applies, so many more permutations of a password are possible, greatly decreasing the chance of a clash and likely always finding it is a unique password.
So the short answer is: the latter code snippet generates many more unique passwords than the former, decreasing the chance of a duplicate and greatly increasing the execution speed.