sqlalchemy-encrypted-field
Encrypted column types for SQLAlchemy. Secrets are ciphertext at rest, plaintext only where you ask for it.
from sqlalchemy_encrypted_field import EncryptedTextField, configure
configure(os.environ["SQLALCHEMY_ENCRYPTED_FIELD_KEYS"])
class Server(Base):
__tablename__ = "servers"
id: Mapped[int] = mapped_column(primary_key=True)
token = EncryptedTextField(default="")
session.add(Server(token="sk-live-..."))
session.commit()
server.token # Secret(<redacted>)
server.token.decrypt() # "sk-live-..."
What it does differently
Every envelope is bound to its column. The associated data is the owning
table.column, so ciphertext copied from one column into another fails to
decrypt. Without this, anyone with write access to the database can move a
low-value token into a high-value column, or one tenant's row into another's,
without ever touching a key.
Key rotation has no flag day. The key setting is a comma-separated list. The first key encrypts, every key decrypts. Rotating means prepending a new key and dropping the old one once nothing needs it, with no downtime and no bulk re-encryption.
Loading a row never decrypts it. Reads hand back a Secret, not a string.
Decryption happens only where decrypt() is called, and repr() is redacted,
so a logged row or a stack trace cannot leak the value.
Failures are named. A dropped key raises SecretDecryptError, whose message
points at the key configuration rather than a bare crypto traceback. Using a
column before configure() raises NotConfiguredError instead of silently
storing plaintext.
Install
pip install sqlalchemy-encrypted-field
Requires Python 3.11+, SQLAlchemy 2.0+. Tested on 3.11 through 3.14.
Fields
| Field | Assign | Read back |
|---|---|---|
EncryptedTextField() |
str |
Secret, use .decrypt() -> str |
EncryptedBytesField() |
bytes |
SecretBytes, use .decrypt() -> bytes |
EncryptedJsonField() |
dict |
SecretsMapping, a lazily-decrypting dict |
All three store LargeBinary and are NOT NULL. Empty values ("", b"") are
stored as empty bytes with no envelope, so a missing secret costs no crypto and
needs no key.
SecretsMapping tracks in-place writes, so row.data["token"] = "new" marks
the column dirty and persists on flush. Reach for it when a secret is genuinely
a mapping; a single value belongs in an EncryptedTextField.
Assigning a loaded Secret to a different column re-encrypts it under that
column's associated data, so moving a value between columns works.
Keys
Generate one:
python -c "import base64, os; print(base64.b64encode(os.urandom(32)).decode())"
configure() takes that string, or a comma-separated list of them. It also
takes a callable returning the string, which is consulted on every operation.
Use the callable form when keys come from settings that can be reloaded at
runtime.
configure(lambda: settings.secrets_key)
validate_keys(value) is exposed separately so the same rules can run inside
your own settings validation and fail at startup. A malformed or absent key is
always an error, never a fallback to plaintext. Validation errors never echo the
key material, because they end up in logs.
Envelope
version[1] || salt[16] || nonce[12] || ciphertext[n] || tag[16]
AES-256-GCM. The salt derives a per-envelope key with HKDF-SHA256 over the
configured master key, so the master never reaches a cipher directly and nonce
reuse across envelopes is not a concern. The table.column string is the
associated data.
configure(..., info=b"...") overrides the HKDF info string. The default suits
new projects. Set it only to keep reading envelopes written by an existing
store that used a different one, and never change it once data exists, because
it is part of the key derivation.
What it is not
This encrypts values so that a database dump, a backup, or a replica is not a pile of readable credentials. It does not protect against an attacker who can read your process memory or your key.
License
MIT
Metadata
Release files for sqlalchemy-encrypted-field 1.0.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sqlalchemy_encrypted_field-1.0.2.tar.gz | 11.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sqlalchemy_encrypted_field-1.0.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 20.7 kB
Release files / sqlalchemy_encrypted_field-1.0.2.tar.gz
| Download URL | sqlalchemy_encrypted_field-1.0.2.tar.gz |
|---|---|
| Size | 11.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5ea80fd66ddc902e1f0f533822f0c760cba91f20b8e7817ea040a015053c117e
|
|
BLAKE2b-256 checksum How to use checksums |
8f81c61f39d4a2846fea3f2bd33a2907a7ae7f0cf6419dabc8e4a04821eb57ae
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 8, 2026.
Transparency logRelease files / sqlalchemy_encrypted_field-1.0.2-py3-none-any.whl
| Download URL | sqlalchemy_encrypted_field-1.0.2-py3-none-any.whl |
|---|---|
| Size | 9.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
83f47f32b092ce8c3b4cb38cc29fb66145b8ce73273caf8995b270becab5a302
|
|
BLAKE2b-256 checksum How to use checksums |
c2f2d547c79c46e2448201bb100bad3da633c375a8367840bd3ef185d722fb02
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 8, 2026.
Transparency log