The Rules
Before we can score a password we have to decide what we're even measuring. "Strong" is a feeling. Code can't act on a feeling. So we turn it into a handful of yes/no questions, one function each:
- Is it long enough?
- Does it contain a lowercase letter?
- An uppercase letter?
- A digit?
- A symbol (anything that isn't a letter or digit)?
Five questions. Five tiny functions. Each one takes a password and returns True or False. That's the whole phase. Keeping them separate matters - later we'll count how many passed, and we'll tell the user which one failed. If we mashed them into one big check we couldn't do either.
One rule at a time
Start with length. Pick a minimum and compare. People love arguing about the number; we'll use 8, because it's the lowest bar most real systems accept. You can change it later in one place.
return >=
# too short
# plenty long
# exactly 8 -> True
See how the function says nothing about whether 12345678 is a good password? It only answers the one question it was asked. That's deliberate. Each rule stays dumb and honest; the smarts come from combining them.
Checking for a kind of character
Now the character classes. We need to know if a password contains at least one lowercase letter, uppercase letter, and digit. Python strings have methods that test a single character: "a".islower(), "A".isupper(), "5".isdigit(). We loop over the password and ask if any character passes.
any(...) is the right tool here. It walks a sequence and returns True the moment one item is true, False if none are. Compare that to writing a loop with a flag variable - any says what we mean in one line.
return
return
return
# no lowercase -> False
# has A B C -> True
# has 1 2 3 -> True
# no digits -> False
The tricky one: symbols
A symbol is "not a letter, not a digit". You could try to list every symbol - !@#$%... - but you'll forget some, and different keyboards have different ones. Don't enumerate. Define a symbol as the absence of letter-and-digit-ness. A character is a symbol if it isn't alphanumeric: not c.isalnum().
One catch: a space is also "not alphanumeric", and so is a tab. For a password checker that's fine - a space is a perfectly good password character and many people use passphrases with spaces. So we'll count anything non-alphanumeric, including spaces, as a symbol. If you ever want to exclude spaces, that's a one-line change you can see in the code.
return
# all letters/digits -> False
# the dash -> True
# the space counts -> True
# the @ -> True
All five rules together
Here's everything from this phase in one block, run against a small set of sample passwords so you can see the rules light up differently for each. This is the first time you'll feel the whole picture: weak passwords fail most rules, strong ones pass most.
return >=
return
return
return
return
=
Run it. Look at the table. "cat" fails almost everything. "P@ssw0rd!" passes everything - even though, as we'll see in the last phase, it's a terrible password that any cracking tool guesses in seconds. That gap is the lesson of this whole project: passing the rules and being safe are not the same thing. The rules are a floor, not a guarantee.
Try it yourself
Edit the samples list. Add your own passwords (don't use real ones). Watch which rules pass. A few things worth poking at:
- Add
"12345678". It's long but fails every character class exceptdigit. The rules already tell you it's lopsided. - Lower the
minimumto 6 inlong_enoughand notice nothing else has to change - that's the payoff of one function per rule.
Next phase we stop reading a table of booleans by eye and let the code do the judging: we'll turn these five True/False answers into a single score and a label.