Modern password hashing for Python — a drop-in replacement for passlib
Project description
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
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 hashward-0.1.0.tar.gz.
File metadata
- Download URL: hashward-0.1.0.tar.gz
- Upload date:
- Size: 34.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af549cd0d7c64f9e5e54be85d87fa4c0ad896037846b37316eaabb6f28196d42
|
|
| MD5 |
0bd1680c6985152d25df93c57ca84bd0
|
|
| BLAKE2b-256 |
b4324a9c4f286dab082719dd433da65c97a5e9d26fe445009e60b674abb3a569
|
Provenance
The following attestation bundles were made for hashward-0.1.0.tar.gz:
Publisher:
publish.yml on agentine/hashward
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hashward-0.1.0.tar.gz -
Subject digest:
af549cd0d7c64f9e5e54be85d87fa4c0ad896037846b37316eaabb6f28196d42 - Sigstore transparency entry: 1091845327
- Sigstore integration time:
-
Permalink:
agentine/hashward@f6610ec324947501b5de2c9c7c9fcff6c53efe7f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/agentine
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f6610ec324947501b5de2c9c7c9fcff6c53efe7f -
Trigger Event:
release
-
Statement type:
File details
Details for the file hashward-0.1.0-py3-none-any.whl.
File metadata
- Download URL: hashward-0.1.0-py3-none-any.whl
- Upload date:
- Size: 29.4 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 |
5ac6a41126f133364741e1d07160cfd7d71da27905340e7c8e5af7671846985f
|
|
| MD5 |
26a711fa635ef5ddf30b6e71ad34bcbe
|
|
| BLAKE2b-256 |
86fa55978c91cb46a3838162cede13d7269d39f3fe4ab9fafe56c85f0aa1f043
|
Provenance
The following attestation bundles were made for hashward-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on agentine/hashward
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hashward-0.1.0-py3-none-any.whl -
Subject digest:
5ac6a41126f133364741e1d07160cfd7d71da27905340e7c8e5af7671846985f - Sigstore transparency entry: 1091845398
- Sigstore integration time:
-
Permalink:
agentine/hashward@f6610ec324947501b5de2c9c7c9fcff6c53efe7f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/agentine
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f6610ec324947501b5de2c9c7c9fcff6c53efe7f -
Trigger Event:
release
-
Statement type: