Skip to main content

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 letter
  • LowercaseRule: Requires at least one lowercase letter
  • DigitRule: Requires at least one digit
  • SymbolRule: Requires at least one special character

Common Patterns

  • NumericOnlyRule: Prevents entirely numeric passwords
  • CommonPasswordRule: Checks against a list of common/weak passwords
  • ContextRule: 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:

  1. Lowercase: All text converted to lowercase
  2. Whitespace stripping: Leading/trailing spaces removed
  3. Leetspeak substitution: 0โ†’o, 1โ†’l, 3โ†’e, 4โ†’a, 5โ†’s, 7โ†’t, @โ†’a, $โ†’s
  4. 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:

  1. All tests pass: pytest tests/
  2. Code is properly typed: mypy src/passguard/
  3. Code is well-documented with docstrings
  4. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

passguard-0.1.0.tar.gz (62.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

passguard-0.1.0-py3-none-any.whl (59.1 kB view details)

Uploaded Python 3

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

Hashes for passguard-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f3306df5ac976409161c1e5dd0525e7fe8ec006dd729ade850d819260b61313c
MD5 fc4632d7e3aef1095f08446631b1a376
BLAKE2b-256 b27106fb41fd5978a181b6ef8b4f1c30c684625bb2adf76140eb714f84d8b958

See more details on using hashes here.

Provenance

The following attestation bundles were made for passguard-0.1.0.tar.gz:

Publisher: manual-release.yml on boluwatife-py/passguard

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for passguard-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0c32f015536be8ceccdcae59d4868e5948dfd1a85df592ffd9e97c3b3876235a
MD5 ac0ea0f0904113cfef35c5febf11eb29
BLAKE2b-256 5be735a61255d96586c9b36c067e10959c806e4f9a0d7d5e3ee512bfd7c0e9ee

See more details on using hashes here.

Provenance

The following attestation bundles were made for passguard-0.1.0-py3-none-any.whl:

Publisher: manual-release.yml on boluwatife-py/passguard

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page