About Imports
Don't import a module and assign a variable name to the attribute of the module.
Don't do this:
import string letters = string.letters
Instead import
the attribute from
the module, and if you need to change the name, use as
to alias it, like this:
from string import ascii_letters as letters, digits, punctuation as special_chars
Naming Variables
Don't name variables like many_letter, many_digit, many_special
, variable names should be concise and expressive, you should know what the variable is for by looking at its name.
Instead use these: num_letters, num_digits, num_special_chars
Typecasting str
to list
Don't convert str
to list
like this: letters_list = list(letters)
, it is absolutely unnecessary and a waste of execution time. str
is already a sequence type, it is an ordered container containing the characters, 'abc'
contains the same data as ['a', 'b', 'c']
, they just have different methods associated with them, but all operations that can be performed on sequences can be performed on str
s.
random
supports choosing from str
s, just use str
s directly in your case.
About Functions
Don't use input
to get variables: many_letter = int(input(f"how many letter ? \n"))
It is inefficient and not reusable, you should put your code into functions to increase reusability, like so:
def generate_password(num_letters, num_digits, num_special_chars): pass
Password Generation
And this is where your code falls flat. Your code is absolutely NOT working as intended, it is objectively non-functional.
The first thing that comes to mind, almost all websites require passwords to contain both uppercase and lowercase letters when registering accounts, I have never found one that is an exception. And your code doesn't guarantee the result will contain both uppercase and lowercase.
Your code doesn't work properly, as other answers have already pointed out, and I won't reiterate.
And you shouldn't allow the user to choose what number of each type of characters a password can have, this is insecure, instead you should randomly choose the number of each category of characters, and only let the user set the length of the characters. And you should set a minimum length requirement, most websites require at least 8 characters long passwords.
Your for
loop can be simplified to something like this:
chars = [] for _ in range(number): chars.append(random.choice(charset))
Then it could also be simplified further to this (list comprehension):
chars = [random.choice(charset) for _ in range(number)]
Which is equivalent to this:
chars = random.choices(charset, k=number)
The latter method is much more efficient than the former, but unfortunately as others pointed out, random
is not cryptographically secure, the secrets
standard library, while cryptographically secure, doesn't have a choices
method.
And you should also shuffle the password to increase entropy.
Fixed code
import random import secrets from functools import reduce from operator import iconcat from string import ascii_lowercase, ascii_uppercase, digits, punctuation from typing import Generator, List CHARSETS = (ascii_lowercase, ascii_uppercase, punctuation, digits) def choose_charset(charset: str, number: int, unique: bool = False) -> List[str]: if not unique: return [secrets.choice(charset) for _ in range(number)] charset = list(charset) choices = [] for _ in range(number): choice = secrets.choice(charset) choices.append(choice) charset.remove(choice) return choices def split(number: int) -> Generator[int, None, None]: for a, b in ((4, 68), (3, 42), (2, 10)): n = max( 2 + secrets.randbelow( number // a - 1 ), number - b ) number -= n yield n if number > 10: raise ValueError('remaining number is too big') yield number def generate_password(length: int, unique: bool = False) -> str: if not isinstance(length, int) or not 8 <= length <= 94: raise ValueError('argument `length` should be an `int` between 8 and 94') password = reduce(iconcat, map(choose_charset, CHARSETS, split(length), [unique]*4), []) random.shuffle(password) return ''.join(password) if __name__ == '__main__': from itertools import product for i, _ in product(range(8, 16), range(3)): print(generate_password(i, True))
Output:
0?rMdW2. ^kXQ<40x 9t0KL:[i J58vRp7_[ {MA7o5e!9 b$2WC6h}3 `45UvdZ;08 91W"8,I!ca .1f2<zUT7( 42-g0y?9UP1 {>NoBk8406$ 3$kZ6_1S2Ez E846_1dA7e&3 t3@5wCu04}~N 0w9834^I&1Ok K43x~82`I10wh Cq7\Q1^d`390v i;085m<FNa42: E)$-2z1KA0L(y7 :9[`S8b7;t16J} 4[,W0Dhl3I75%1 36ejCJ5b20E|8_; jR17C8H&N,925vl 5Q8LK$f\%uy3;46
Update
I have modified the code so that it now can generate passwords containing only unique characters, and I have limited the number of each type of characters the password can have, so that the number of characters per type the password can have is approximately the same.
Because I have added the functionality to generate only passwords containing unique characters, the length of passwords is limited to 94 characters. Because there are only 94 symbols in total, a password containing more than 94 characters is guaranteed to contain duplicate characters.
finalpassword
is constructed, can be replaced by therandom.shuffle
method.\$\endgroup\$