4
\$\begingroup\$

I have spent some time with python. I got an idea of creating a random password generator. It generates passwords consisting of random characters. The user can exclude different types of characters (letters, numbers, symbols) from the generated password and can customize the length of the generated password. The code consists of a single module, by the way. I am using python 3.13.0-rc1, if that is necessary. I am new here, so please pardon and mention if there is any mistake. Here is my main.py:

#####################################################
# Name: pwgen
# Author: Muhammad Altaaf <[email protected]>
# Description: A random password generator in your
# toolset.
#####################################################
from __future__ import annotations
import secrets
import string
import sys
import typing
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from dataclasses import dataclass
if typing.TYPE_CHECKING:
 from argparse import Namespace
PROG_NAME = "pwgen"
PROG_DESC = """
██████╗ ██╗ ██╗ ██████╗ ███████╗███╗ ██╗
██╔══██╗██║ ██║██╔════╝ ██╔════╝████╗ ██║
██████╔╝██║ █╗ ██║██║ ███╗█████╗ ██╔██╗ ██║
██╔═══╝ ██║███╗██║██║ ██║██╔══╝ ██║╚██╗██║
██║ ╚███╔███╔╝╚██████╔╝███████╗██║ ╚████║
╚═╝ ╚══╝╚══╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝
A random password generator in your toolset.
"""
PROG_VERSION = "1.1.0"
PROG_AUTHOR = "Muhammad Altaaf"
PROG_AUTHOR_CONTACT = "[email protected]"
PROG_EPILOG = f"""\
Version {PROG_VERSION}.
Written by {PROG_AUTHOR} <{PROG_AUTHOR_CONTACT}>.
"""
@dataclass(frozen=True)
class _Config:
 letters: bool = True
 digits: bool = True
 punct: bool = True
 length: int = 8
 def __post_init__(self):
 if not (self.letters or self.digits or self.punct):
 print(
 "At least one of the three components (alphabets, numbers or \
 symbols) must be allowed",
 file=sys.stderr,
 )
 sys.exit(1)
def _gen_passwd(config: _Config) -> str:
 """The main password generator."""
 passwd_len = config.length
 # string to get password characters from
 base = ""
 # the password
 passwd = ""
 if config.letters:
 base += string.ascii_letters
 if config.digits:
 base += string.digits
 if config.punct:
 base += string.punctuation
 for _ in range(passwd_len):
 while (char := secrets.choice(base)) == "\\":
 continue
 passwd += char
 return passwd
def parse_opts() -> Namespace:
 """Parse command line options and return Namespace object."""
 o_parser = ArgumentParser(
 prog=PROG_NAME,
 description=PROG_DESC,
 epilog=PROG_EPILOG,
 formatter_class=RawDescriptionHelpFormatter,
 )
 add_opt = o_parser.add_argument
 add_opt(
 "-a",
 "--alphabets",
 action="store_true",
 help="Don't include alphabets in the password. (Default is to include)",
 )
 add_opt(
 "-n",
 "--numbers",
 action="store_true",
 help="Don't include numbers in the password. (Default is to include)",
 )
 add_opt(
 "-p",
 "--punctuation",
 action="store_true",
 help="Don't include symbols in the password. (Default is to include)",
 )
 add_opt("-l",
 "--length",
 type=int,
 help="Length of the password. (Default is 8)",
 )
 return o_parser.parse_args()
def gen_config(cmd_opts: Namespace) -> _Config:
 """Generate config from arguments."""
 incl_letters = not cmd_opts.alphabets
 incl_digits = not cmd_opts.numbers
 incl_punct = not cmd_opts.punctuation
 p_len = cmd_opts.length or 8
 config = _Config(incl_letters, incl_digits, incl_punct, p_len)
 return config
def gen_passwd() -> str:
 """Wrapper function for putting all things together."""
 options = parse_opts()
 config = gen_config(options)
 passwd: str = _gen_passwd(config)
 return passwd
def main():
 passwd = gen_passwd()
 print(f"Password: {passwd}")
if __name__ == "__main__":
 main()
toolic
14.4k5 gold badges29 silver badges201 bronze badges
asked Aug 30, 2024 at 11:38
\$\endgroup\$
3
  • 3
    \$\begingroup\$ I encourage you to carefully read codereview.stackexchange.com/questions/292925/… \$\endgroup\$ Commented Aug 30, 2024 at 11:43
  • \$\begingroup\$ Please don't call it pwgen - a well-known unix utility exists with that name, and several other libraries/packages already reuse that name. \$\endgroup\$ Commented Aug 30, 2024 at 23:58
  • \$\begingroup\$ @STerliakov Oh, I am seeing the utility the first time \$\endgroup\$ Commented Aug 31, 2024 at 2:28

1 Answer 1

5
\$\begingroup\$

Indeed, there is a good reference topic so without rehashing what has already been said, a couple remarks though:

Unnecessary variable duplication

In _gen_passwd:

passwd_len = config.length

Just use config.length

Further, the variable concatenation is not really needed.

A more straightforward approach would involve a list comprehension along these lines:

return ''.join(secrets.choice(base) for i in range(config.length))

Parsing arguments

I haved mixed feelings about your _Config dataclass, because it does very little in fact. You check that:

At least one of the three components (alphabets, numbers or symbols) must be allowed

This is something that should preferably be handled with argparse, the module is underutilized. You can set default values for your CLI arguments and even apply a range for the password length for example. Custom validation routines can be added if the built-in filters are not sufficient.

It is possible to have toggle boolean arguments, for example --no-alphabets to negate --alphabets. See here for some tips. Accordingly your code could be adapted like this since you are using Python > 3.9:

add_opt('--alphabets', action=argparse.BooleanOptionalAction, default=True,
 help="Don't include alphabets in the password. (Default is to include)")
add_opt('--numbers', action=argparse.BooleanOptionalAction, default=True,
 help="Don't include numbers in the password. (Default is to include)")
add_opt('--punctuation', action=argparse.BooleanOptionalAction, default=True,
 help="Don't include symbols in the password. (Default is to include)")
add_opt("-l",
 "--length",
 type=int, default=8,
 help="Length of the password. (Default is 8)",
 )
args = o_parser.parse_args()
if not (args.alphabets or args.numbers or args.punctuation):
 o_parser.error("At least one of the three components (alphabets, numbers or symbols) must be allowed")
return args

(Maybe there is an even better way to ensure that at least one of the three parameters is True).

One minor downside is that --no-alphabets and --alphabets are not mutually exclusive on the command line, the last option will prevail but you could still use parser.add_mutually_exclusive_group to improve further. So I believe the dataclass is redundant and not useful here.

answered Aug 30, 2024 at 16:07
\$\endgroup\$
1
  • \$\begingroup\$ I wrote "full" for loop (for _ in...) for readability \$\endgroup\$ Commented Aug 31, 2024 at 2:39

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.