# Security

The [documentation for the `random` module](https://docs.python.org/3/library/random.html) 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 [`secrets`](https://docs.python.org/3/library/secrets.html) module.

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.