How to Generate Memorable Password
I have made several Python password generators. One consistent goal of my generators is its password usability; memorized or not, a user should be able to type it without the worry of user errors. The key here is “type”, because, I’ve learnt the hard way that some password policies out in the wild are purely sadistic. I won’t name names, but one website I use frequently doesn’t allow copy-pasting, no extensions, and virtual keyboards only, on both mobile and desktop environment. Memorizing a password by muscle memory is simply not possible; whoever came up with that idea must have thought we should use post-its on monitors.
My goal with this particular script is rather simple. Password generators, like 1Password, do offer generator memorable or smart password recipes. However, it is based on word counts (e.g. 4 words long). But that doesn’t make any guarantee it will fit into these sadistic password policies I have seen, as small as 12 characters, some even 8, with alphanumeric and special characters requirements.
The script, as always in Python, comes in two folds: wordlist and the .py script. I decided to use 1Password-replacement list, but there are other wordlists out there, even on the same GitHub repository. Just be sure to put the wordlist file in the same folder as the Python script.
As for the script itself, it has the usual variables. SPECIAL_CHARS for ones that are permitted by the policy. Update WORDLIST_FILE to match the actual wordlist text file. Inside the function itself, you would notice I have given the default values, which you can change. total for the total length of the password. mix_capital, if both uppercase and lowercase must be included. max_word for the maximum length of each word. If you have a problem generating a password, consider lowering the value. And lastly, min_digits and min_specials, on how many do you need of each per policy — they are used entirely as delimiter in this script.
from pathlib import Path
import secrets
import string
SPECIAL_CHARS = ['!', '@', '#', '$', '^', '&']
WORDLIST_FILE = "1password-replacement.txt"
def memorable_password_generator(total=16, mix_capital = True, max_word=6, min_digits=1, min_specials=1):
script_dir = Path(__file__).resolve().parent
file_path = script_dir / WORDLIST_FILE
word_cache = {}
try:
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
word = line.strip().lower()
if word:
word_cache.setdefault(len(word), []).append(word)
except FileNotFoundError:
return f"Error: '{WORDLIST_FILE}' not found."
shortest_word = min(word_cache.keys())
digit_count = min_digits
special_count = min_specials
remaining = total - digit_count - special_count
words = []
# Step 1-4: pick words until budget is spent
while True:
while remaining > 0:
if remaining < shortest_word:
# Can't fit another word — absorb leftover into digit slots
digit_count += remaining
break
length = secrets.choice(list(word_cache.keys()))
if length > remaining or length > max_word:
continue
words.append(secrets.choice(word_cache[length]))
remaining -= length
if mix_capital and len(words) == 1:
continue
else:
break
# Build delimiter slots: [before word_0] [between ...] [trailing]
# Always 'e' at both ends; middle slots are random among 'e', 'n', 's'.
# 'e' = empty | 'n' = holds digits | 's' = holds specials
while True:
delimiter_slots = ['e']
for _ in range(len(words) - 1):
delimiter_slots.append(secrets.choice('ens'))
delimiter_slots.append('e')
delimiter_slots = [{"cat": kind, "val": ""} for kind in delimiter_slots]
if digit_count > 0 and not any(s['cat'] == 'n' for s in delimiter_slots):
empty_positions = [i for i, s in enumerate(delimiter_slots) if s['cat'] == 'e']
if not empty_positions:
continue
delimiter_slots[secrets.choice(empty_positions)]['cat'] = 'n'
if special_count > 0 and not any(s['cat'] == 's' for s in delimiter_slots):
empty_positions = [i for i, s in enumerate(delimiter_slots) if s['cat'] == 'e']
if not empty_positions:
continue
delimiter_slots[secrets.choice(empty_positions)]['cat'] = 's'
break
# Distribute digits across 'n' slots, specials across 's' slots
while digit_count > 0:
eligible = [i for i, s in enumerate(delimiter_slots) if s['cat'] == 'n']
delimiter_slots[secrets.choice(eligible)]['val'] += secrets.choice(string.digits)
digit_count -= 1
while special_count > 0:
eligible = [i for i, s in enumerate(delimiter_slots) if s['cat'] == 's']
delimiter_slots[secrets.choice(eligible)]['val'] += secrets.choice(SPECIAL_CHARS)
special_count -= 1
# Assemble: each slot contributes its value, then the next word (if any).
# When two words are adjacent (no visible separator), alternate case
# so HORSEstaple stays readable instead of HORSESTAPLE.
password = ""
prev_was_upper = False
word_idx = 0
has_upper = False
for slot in delimiter_slots:
password += slot['val']
if word_idx < len(words):
word = words[word_idx]
has_separator = slot['val'] != ''
if not has_separator and word_idx > 0:
# Adjacent to previous word — alternate case for readability
if prev_was_upper:
word = word.lower()
else:
word = word.upper()
has_upper = True
else:
# Separator present, or this is the first word — random case
if secrets.choice([True, False]):
word = word.upper()
has_upper = True
else:
word = word.lower()
prev_was_upper = word[0].isupper()
if not has_upper and word_idx == (len(words) - 1):
word = word.upper()
password += word
word_idx += 1
return password
if __name__ == "__main__":
print(memorable_password_generator())
I’ve tested the script on macOS and iOS (Pythonista). It should work on any platform. The result should come out near instantaneous, so if you happen to dislike what the generator outputs — run it again.

Comments will be automatically closed after 30 days.