hashward
Modern password hashing for Python. A drop-in replacement for passlib, which has been unmaintained since 2020 and is broken on Python 3.13+.
Why hashward?
- passlib is dead. Last release was October 2020. It crashes on Python 3.13+ due to the removed
cryptmodule. - Zero required dependencies. Pure Python implementations for all hashlib-based schemes. Optional deps for argon2 and bcrypt.
- Python 3.9+ only. No Python 2 baggage. Full type hints with
py.typedmarker. - passlib-compatible API.
CryptContext, hash identification, andverify_and_update()all work the same way. - Secure defaults. Argon2id as the default scheme with safe parameter defaults and timing-safe comparisons.
Installation
pip install hashward
With argon2 support (recommended):
pip install hashward[argon2]
With bcrypt support:
pip install hashward[bcrypt]
With everything:
pip install hashward[all]
Quick Start
Simple hashing and verification
import hashward
# Hash a password (uses argon2id by default)
hashed = hashward.hash("my secret password")
# '$argon2id$v=19$m=65536,t=2,p=2$...'
# Verify a password
hashward.verify("my secret password", hashed) # True
hashward.verify("wrong password", hashed) # False
# Use a specific scheme
hashed = hashward.hash("my secret password", scheme="bcrypt")
# '$2b$12$...'
CryptContext for policy management
from hashward import CryptContext
# Create a context with your preferred schemes
ctx = CryptContext(
schemes=["argon2", "bcrypt", "pbkdf2_sha256"],
default="argon2",
deprecated=["pbkdf2_sha256"],
argon2__time_cost=3,
argon2__memory_cost=65536,
)
# Hash and verify
hashed = ctx.hash("password")
assert ctx.verify("password", hashed)
# Automatic scheme identification
ctx.identify(hashed) # "argon2"
# Check if a hash needs upgrading (deprecated scheme or outdated params)
ctx.needs_update(hashed) # False (it's current)
# Verify and get a new hash if the old one needs upgrading
valid, new_hash = ctx.verify_and_update("password", old_bcrypt_hash)
if valid and new_hash:
# Save new_hash to your database — it's been upgraded to argon2
save_to_db(new_hash)
Hash identification
from hashward import identify
identify("$argon2id$v=19$m=65536,t=2,p=2$...") # "argon2"
identify("$2b$12$...") # "bcrypt"
identify("$6$rounds=656000$...") # "sha512_crypt"
identify("$1$...") # "md5_crypt"
identify("pbkdf2_sha256$600000$...") # "django_pbkdf2_sha256"
INI configuration (passlib-compatible)
from hashward import CryptContext
# Load from INI-format string
config = """
[hashward]
schemes = argon2, bcrypt, pbkdf2_sha256
default = argon2
deprecated = pbkdf2_sha256
argon2__time_cost = 3
argon2__memory_cost = 65536
"""
ctx = CryptContext.from_string(config)
# Serialize back
print(ctx.to_string())
Supported Schemes
Modern (recommended for new hashes)
| Scheme | Backend | Notes |
|---|---|---|
argon2 |
argon2-cffi | Default. Argon2id, memory-hard. Requires pip install hashward[argon2]. |
bcrypt |
bcrypt | Industry standard. Requires pip install hashward[bcrypt]. |
bcrypt_sha256 |
bcrypt | Bcrypt with SHA-256 pre-hash (no 72-byte limit). |
scrypt |
hashlib.scrypt (stdlib) |
Memory-hard. No extra dependencies. |
pbkdf2_sha256 |
hashlib.pbkdf2_hmac (stdlib) |
NIST-approved. No extra dependencies. |
pbkdf2_sha512 |
hashlib.pbkdf2_hmac (stdlib) |
NIST-approved. No extra dependencies. |
Legacy (verification and migration only)
| Scheme | Notes |
|---|---|
sha512_crypt |
Common in /etc/shadow. Pure Python, no crypt module needed. |
sha256_crypt |
Common in /etc/shadow. Pure Python, no crypt module needed. |
md5_crypt |
Insecure. Always reports needs_update. |
des_crypt |
Insecure. Always reports needs_update. |
Django compatibility
| Scheme | Notes |
|---|---|
django_pbkdf2_sha256 |
Django's default hasher format. |
django_bcrypt |
Django bcrypt format. |
django_bcrypt_sha256 |
Django bcrypt+SHA-256 format. |
django_argon2 |
Django argon2 format. |
django_scrypt |
Django scrypt format. |
Migrating from passlib
hashward provides a compatibility layer for gradual migration.
Step 1: Replace imports
# Before (passlib):
from passlib.context import CryptContext
# After (hashward — direct):
from hashward import CryptContext
# Or use the compat shim for minimal changes:
from hashward.compat.passlib import CryptContext
Step 2: Update context configuration
# passlib config:
ctx = CryptContext(schemes=["argon2", "bcrypt", "pbkdf2_sha256"])
# hashward config (identical API):
ctx = CryptContext(schemes=["argon2", "bcrypt", "pbkdf2_sha256"])
Step 3: Verify existing hashes still work
hashward can verify hashes generated by passlib. All passlib hash formats are recognized, including bcrypt_sha256 v1 and v2 formats.
from hashward import CryptContext
ctx = CryptContext(schemes=["argon2", "bcrypt", "pbkdf2_sha256"], deprecated=["pbkdf2_sha256"])
# Verify a passlib-generated hash
old_hash = "$pbkdf2-sha256$29000$..." # generated by passlib
valid = ctx.verify("password", old_hash)
# Automatically upgrade old hashes
valid, new_hash = ctx.verify_and_update("password", old_hash)
if valid and new_hash:
# new_hash is argon2id — save it
save_to_db(new_hash)
API Reference
Module-level functions
hashward.hash(secret, scheme="argon2")
Hash a password using the specified scheme (default: argon2).
hashward.verify(secret, hash_string)
Verify a password against a hash string. Automatically identifies the scheme.
hashward.identify(hash_string)
Detect the hashing scheme from a hash string. Returns the scheme name or None.
CryptContext
CryptContext(schemes, default, deprecated, **settings)
Create a policy manager for password hashing.
schemes— list of allowed scheme namesdefault— scheme to use for new hashesdeprecated— list of schemes that triggerneeds_update()min_verify_time— minimum wall-clock seconds forverify()(default:0, disabled). Useful for preventing timing-based user enumeration.truncate_error— ifTrue, raisePasswordValueErrorwhen a bcrypt password exceeds 72 bytes instead of silently truncating (default:False)**settings— per-scheme settings usingscheme__param=valuesyntax
ctx.hash(secret, scheme=None, **settings)
Hash a password. Uses the default scheme unless overridden.
ctx.verify(secret, hash)
Verify a password against a hash. Returns False for unrecognized hashes.
ctx.identify(hash)
Identify the scheme of a hash string. Only returns schemes configured in this context.
ctx.needs_update(hash)
Check if a hash needs re-hashing (deprecated scheme or outdated parameters).
ctx.verify_and_update(secret, hash)
Verify and return (valid, new_hash). new_hash is None if no update is needed.
ctx.using(**overrides)
Return a new CryptContext with overridden settings.
ctx.copy(**overrides)
Alias for ctx.using().
ctx.to_string() / CryptContext.from_string(ini_str)
Serialize to / deserialize from INI-format configuration strings.
Security Considerations
- Default scheme is argon2id with memory-hard parameters (64 MiB, 2 iterations, 2 threads).
- Timing-safe comparisons via
hmac.compare_digestfor all hash verification. - No
cryptmodule dependency. All legacy schemes (SHA-crypt, MD5-crypt, DES-crypt) use pure Python implementations. - DES-crypt always reports
needs_update. Any existing DES-crypt hash will trigger automatic re-hashing on the nextverify_and_update()call. Do not use it for new hashes. - bcrypt 72-byte limit is handled: passwords are silently truncated. Use
bcrypt_sha256ortruncate_error=Truein CryptContext for explicit control.
Development
# Clone and set up
git clone https://github.com/agentine/hashward.git
cd hashward
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
# Run tests
pytest tests/ -v
# Run benchmarks
python benchmarks/bench_hashing.py
License
MIT
Metadata
Release files for hashward 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hashward-0.1.0.tar.gz | 34.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hashward-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 63.9 kB
Release files / hashward-0.1.0.tar.gz
| Download URL | hashward-0.1.0.tar.gz |
|---|---|
| Size | 34.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
af549cd0d7c64f9e5e54be85d87fa4c0ad896037846b37316eaabb6f28196d42
|
|
BLAKE2b-256 checksum How to use checksums |
b4324a9c4f286dab082719dd433da65c97a5e9d26fe445009e60b674abb3a569
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 12, 2026.
Transparency logRelease files / hashward-0.1.0-py3-none-any.whl
| Download URL | hashward-0.1.0-py3-none-any.whl |
|---|---|
| Size | 29.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5ac6a41126f133364741e1d07160cfd7d71da27905340e7c8e5af7671846985f
|
|
BLAKE2b-256 checksum How to use checksums |
86fa55978c91cb46a3838162cede13d7269d39f3fe4ab9fafe56c85f0aa1f043
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 12, 2026.
Transparency log