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()
1 Answer 1
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.
-
\$\begingroup\$ I wrote "full" for loop (
for _ in...
) for readability \$\endgroup\$taafuuu__– taafuuu__2024年08月31日 02:39:24 +00:00Commented Aug 31, 2024 at 2:39
pwgen
- a well-known unix utility exists with that name, and several other libraries/packages already reuse that name. \$\endgroup\$