Comprehensive password validation library with customizable rules, context-aware validation, and Pydantic integration
Project description
PassGuard
A comprehensive, production-ready password validation library for Python with customizable rules, context-aware validation, and Pydantic integration.
Features
- ๐ Flexible Rule System: Built-in rules for length, character requirements, common password detection, and more
- ๐ฏ Context-Aware Validation: Prevent passwords containing usernames, emails, or personal information
- ๐ Rich Validation Results: Detailed feedback with issue codes, severity levels, and validation scores
- ๐จ Customizable Policies: Create your own validation policies or modify the default one
- ๐ Pydantic Integration: First-class support for Pydantic v2 models with validators and custom types
- โป๏ธ Pluggable Architecture: Easy to extend with custom rules and providers
- โก High Performance: Efficient validation with minimal dependencies
Installation
pip install passguard
With Pydantic support:
pip install passguard[pydantic]
Quick Start
Basic Usage
from passguard import PasswordValidator
# Create a validator with default policy
validator = PasswordValidator()
# Evaluate a password
result = validator.evaluate(
"MySecurePass123!",
context_data={"username": "john", "email": "john@example.com"}
)
# Check results
if result.valid:
print(f"โ Password is valid (score: {result.score})")
else:
print(f"โ Password has issues:")
for issue in result.issues:
print(f" - {issue.code}: {issue.message} ({issue.severity})")
Custom Policy
from passguard.policy import PasswordPolicy
from passguard.rules.length import MinLengthRule
from passguard.rules.character import UppercaseRule, DigitRule
# Create a custom policy
policy = PasswordPolicy()
policy.add_rule(MinLengthRule(min_length=12))
policy.add_rule(UppercaseRule())
policy.add_rule(DigitRule())
# Use it in the validator
validator = PasswordValidator(policy=policy)
result = validator.evaluate("MyPassword123")
Disable Specific Rules
# Disable rules you don't want to enforce
validator = PasswordValidator(
disable_rules=["common_password", "numeric_only"]
)
result = validator.evaluate("password123")
Pydantic Integration
With Field Validator
from pydantic import BaseModel, field_validator
from passguard.integrations.pydantic import PasswordField
from passguard.policy import PasswordPolicy
policy = PasswordPolicy.default()
validator_func = PasswordField.make_validator(
policy=policy,
min_score=80,
context_fields=["username", "email"]
)
class RegisterRequest(BaseModel):
username: str
email: str
password: str
@field_validator('password')
@classmethod
def validate_password(cls, v, info):
return validator_func(v, info)
# Use it
try:
user = RegisterRequest(
username="john",
email="john@example.com",
password="SecurePass123!"
)
except ValidationError as e:
print(e)
With PasswordStr Type
from pydantic import BaseModel
from passguard.integrations.pydantic import PasswordStr
class RegisterRequest(BaseModel):
username: str
password: PasswordStr
# Password is automatically validated
user = RegisterRequest(
username="john",
password="SecurePass123!"
)
Built-in Rules
Length Rules
MinLengthRule: Enforces minimum password length (default: 8)
Character Rules
UppercaseRule: Requires at least one uppercase letterLowercaseRule: Requires at least one lowercase letterDigitRule: Requires at least one digitSymbolRule: Requires at least one special character
Common Patterns
NumericOnlyRule: Prevents entirely numeric passwordsCommonPasswordRule: Checks against a list of common/weak passwordsContextRule: Prevents passwords containing personal information (username, email, names)
API Reference
PasswordValidator
Main validator class for password evaluation.
validator = PasswordValidator(
policy=None, # Custom PasswordPolicy (default: PasswordPolicy.default())
disable_rules=None # List of rule codes to disable
)
result = validator.evaluate(
password, # str: Password to validate
context_data=None, # dict: User info (username, email, first_name, etc.)
min_score=100 # int: Minimum score threshold (0-100)
)
ValidationResult
Result object containing validation outcome and details.
result = ValidationResult(
valid: bool, # Overall validation status
score: int, # Validation score (0-100)
issues: List[Issue] # List of validation issues
)
# Methods
result.to_dict() # Serialize to JSON-safe dict
result.has_critical_issues # Property: True if any CRITICAL issues
result.issue_codes # Property: Set of issue codes
Issue
Individual validation issue.
issue = Issue(
code: str, # Machine-readable identifier
message: str, # Human-readable message
severity: Severity # LOW, MEDIUM, HIGH, or CRITICAL
)
issue.to_dict() # Serialize to JSON-safe dict
PasswordPolicy
Configuration for which rules to enforce.
# Get default policy
policy = PasswordPolicy.default()
# Create custom policy
policy = PasswordPolicy()
# Manage rules
policy.add_rule(rule) # Add a rule
policy.remove_rule(RuleClass) # Remove all rules of a type
policy.get_rule(RuleClass) # Get rule instance by type
Context
Normalized user context used in validation.
context = Context(
username=None,
email=None,
first_name=None,
second_name=None,
full_name=None
)
# Create from dictionary
context = Context.from_mapping({
"username": "john",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe"
})
Validation Scores
PassGuard calculates a validation score based on the percentage of rules passed:
- 100: All rules passed
- 75: 75% of rules passed
- 50: 50% of rules passed
- 0: No rules passed
The min_score parameter can be used to enforce a minimum threshold.
Severity Levels
Issues are categorized by severity:
- LOW: Minor suggestions (e.g., could be stronger)
- MEDIUM: Standard requirement not met (e.g., missing uppercase)
- HIGH: Important security requirement not met (e.g., common password)
- CRITICAL: Must-have requirement failed (validation fails if present)
Creating Custom Rules
Extend the PasswordRule base class:
from passguard.rules.base import PasswordRule
from passguard.results import Issue, Severity
class MyCustomRule(PasswordRule):
code = "my_custom_rule"
severity = Severity.MEDIUM
def __init__(self, severity=None):
super().__init__(severity)
# Your initialization
def check(self, password: str, context) -> Issue | None:
"""
Validate password against this rule.
Returns:
Issue if validation fails, None if passes
"""
if some_condition(password):
return Issue(
code=self.code,
message="Your error message",
severity=self.severity
)
return None
# Use it
policy = PasswordPolicy()
policy.add_rule(MyCustomRule())
Text Normalization
PassGuard normalizes text for comparison in context-aware rules:
- Lowercase: All text converted to lowercase
- Whitespace stripping: Leading/trailing spaces removed
- Leetspeak substitution:
0โo,1โl,3โe,4โa,5โs,7โt,@โa,$โs - Non-alphanumeric removal: Special characters stripped
This ensures that variations of personal information are caught (e.g., "J0hn" matches "john").
Configuration
The default policy enforces:
- Minimum 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one digit
- At least one special character
- Cannot be entirely numeric
- Cannot be a common/weak password
- Cannot contain personal information
Customize any of these by creating your own policy.
Examples
Strict Policy
from passguard.policy import PasswordPolicy
from passguard.rules.length import MinLengthRule
from passguard.rules.character import UppercaseRule, LowercaseRule, DigitRule, SymbolRule
policy = PasswordPolicy()
policy.add_rule(MinLengthRule(min_length=16))
policy.add_rule(UppercaseRule())
policy.add_rule(LowercaseRule())
policy.add_rule(DigitRule())
policy.add_rule(SymbolRule())
validator = PasswordValidator(policy=policy)
Lenient Policy
from passguard.policy import PasswordPolicy
from passguard.rules.length import MinLengthRule
from passguard.rules.context import ContextRule
policy = PasswordPolicy()
policy.add_rule(MinLengthRule(min_length=6))
policy.add_rule(ContextRule())
validator = PasswordValidator(policy=policy)
API Endpoint with Validation
from fastapi import FastAPI, HTTPException
from passguard import PasswordValidator
app = FastAPI()
validator = PasswordValidator()
@app.post("/register")
def register(username: str, password: str):
result = validator.evaluate(
password,
context_data={"username": username}
)
if not result.valid:
raise HTTPException(
status_code=400,
detail={
"valid": result.valid,
"score": result.score,
"issues": [issue.to_dict() for issue in result.issues]
}
)
return {"message": "User registered successfully"}
Testing
Run the test suite:
pytest tests/
With coverage:
pytest tests/ --cov=src/passguard
Project Structure
passguard/
โโโ src/passguard/
โ โโโ __init__.py
โ โโโ validator.py # Main PasswordValidator class
โ โโโ policy.py # PasswordPolicy configuration
โ โโโ results.py # ValidationResult, Issue, Severity
โ โโโ context.py # User context normalization
โ โโโ errors.py # Custom exceptions
โ โโโ rules/ # Built-in password rules
โ โ โโโ base.py # PasswordRule base class
โ โ โโโ length.py # MinLengthRule
โ โ โโโ character.py # Character requirement rules
โ โ โโโ numeric.py # NumericOnlyRule
โ โ โโโ common.py # CommonPasswordRule
โ โ โโโ context.py # ContextRule
โ โโโ integrations/ # Framework integrations
โ โ โโโ pydantic.py # Pydantic v2 integration
โ โโโ providers/ # External data providers
โ โโโ utils/ # Utility functions
โ โ โโโ loaders.py # Data loading (gzip)
โ โ โโโ normalize.py # Text normalization
โ โโโ data/ # Data files
โ โโโ common-passwords.txt.gz
โโโ tests/ # Test suite
Performance
PassGuard is optimized for performance:
- Common password checks use efficient set lookups
- Lazy loading of data files with caching
- Minimal object allocation
- Pure Python with no C extensions required
Typical validation takes < 1ms on modern hardware.
Contributing
Contributions are welcome! Please ensure:
- All tests pass:
pytest tests/ - Code is properly typed:
mypy src/passguard/ - Code is well-documented with docstrings
- New rules include comprehensive tests
License
MIT License - see LICENSE file for details.
Changelog
See CHANGELOG.md for version history and updates.
Support
For issues, feature requests, or questions, please open an issue on GitHub.
Built with โค๏ธ for secure password validation
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file passguard-0.1.0.tar.gz.
File metadata
- Download URL: passguard-0.1.0.tar.gz
- Upload date:
- Size: 62.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f3306df5ac976409161c1e5dd0525e7fe8ec006dd729ade850d819260b61313c
|
|
| MD5 |
fc4632d7e3aef1095f08446631b1a376
|
|
| BLAKE2b-256 |
b27106fb41fd5978a181b6ef8b4f1c30c684625bb2adf76140eb714f84d8b958
|
Provenance
The following attestation bundles were made for passguard-0.1.0.tar.gz:
Publisher:
manual-release.yml on boluwatife-py/passguard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
passguard-0.1.0.tar.gz -
Subject digest:
f3306df5ac976409161c1e5dd0525e7fe8ec006dd729ade850d819260b61313c - Sigstore transparency entry: 923457358
- Sigstore integration time:
-
Permalink:
boluwatife-py/passguard@76e1152f3e437e040979ef44d9cfb8b841c02065 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/boluwatife-py
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
manual-release.yml@76e1152f3e437e040979ef44d9cfb8b841c02065 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file passguard-0.1.0-py3-none-any.whl.
File metadata
- Download URL: passguard-0.1.0-py3-none-any.whl
- Upload date:
- Size: 59.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c32f015536be8ceccdcae59d4868e5948dfd1a85df592ffd9e97c3b3876235a
|
|
| MD5 |
ac0ea0f0904113cfef35c5febf11eb29
|
|
| BLAKE2b-256 |
5be735a61255d96586c9b36c067e10959c806e4f9a0d7d5e3ee512bfd7c0e9ee
|
Provenance
The following attestation bundles were made for passguard-0.1.0-py3-none-any.whl:
Publisher:
manual-release.yml on boluwatife-py/passguard
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
passguard-0.1.0-py3-none-any.whl -
Subject digest:
0c32f015536be8ceccdcae59d4868e5948dfd1a85df592ffd9e97c3b3876235a - Sigstore transparency entry: 923457359
- Sigstore integration time:
-
Permalink:
boluwatife-py/passguard@76e1152f3e437e040979ef44d9cfb8b841c02065 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/boluwatife-py
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
manual-release.yml@76e1152f3e437e040979ef44d9cfb8b841c02065 -
Trigger Event:
workflow_dispatch
-
Statement type: