Security
The [documentation for the random module]documentation for the random module gives the following warning:
Warning: The pseudo-random generators of this module should not be used for security purposes. For security or cryptographic uses, see the
secretsmodule.
The secrets module can be used in a similar way to the random module, but uses the best source of randomness available on your system. However, it doesn't provide an equivalent method to shuffle, so you'll have to rework your algorithm a bit. You can take inspiration from the "Recipes and best practices" section of the secrets documentation.
Index into string
Instead of using lists of single-character strings, you can index directly into strings:
>>> foo = 'abc' >>> foo[1] 'b' Taking advantage of this would make the definitions at the beginning of the program easier to write and more readable.
Note that strings are immutable, you can't use string indexing to change characters:
>>> foo[1] = 'd' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'str' object does not support item assignment In your case, changing the character sets would likely be a bug, so string immutability will catch these.
Use built-ins when possible
Even better, the string modules provide these constant strings, so the character set definitions can be simplified to:
from string import ascii_letters, digits, punctuation This would ensure that no character character is omitted or duplicated, and reduce complexity, whether for writing the code or proofreading it.
You could also prefer to use ascii_lowercase and ascii_uppercase for more fine-grained control.
Factor code into functions
This improves reusability, testability and readability of your code. @ggorlen provides good feedback on how to do that.