not-jwt (Python)
A tiny HMAC-SHA256 message signer for Python, with no dependencies. It gives you one simple contract:
sign(message)-> signed stringverify(signed_message)-> original message (or raises)
Use it when you want JWT-like tamper protection without full JWT complexity.
Tokens are interchangeable with every other not-jwt implementation (JavaScript, Go, Rust): sign in one, verify in another. See the main README for an overview.
Install
pip install not-jwt
Quick start
from not_jwt import Signer
signer = Signer("super-secret-key")
token = signer.sign("hello")
message = signer.verify(token) # "hello"
API
class Signer:
def __init__(self, key: str) -> None: ...
def sign(self, message: str) -> str: ...
def verify(self, signed_message: str) -> str: ...
Signer raises TypeError if key is not a string and ValueError if it is
empty. sign raises TypeError if message is not a string, and replaces lone
surrogates in it with U+FFFD. A Signer is safe for concurrent use.
verify raises one of these (both subclass NotJwtError):
InvalidSignedMessageError: not valid base64url, too short to hold a signature, or the message is not valid UTF-8.SignatureVerificationError: the signature does not match.
Using as a JWT replacement
For many internal apps, this can replace JWT when you do not need RFC JWT features.
Typical pattern:
- Put claims into a JSON payload.
- Add
exp(expiry) yourself. - Sign the serialized payload.
- On verify, recover payload from
verify(...), then parse and validate claims/expiry.
Example:
import json
import os
import time
from typing import Optional
from not_jwt import NotJwtError, Signer
signer = Signer(os.environ["AUTH_SECRET"])
def create_token(sub: str, role: str) -> str:
claims = {"sub": sub, "role": role, "exp": int(time.time()) + 60 * 60} # 1 hour
return signer.sign(json.dumps(claims))
def verify_token(token: str) -> Optional[dict]:
try:
claims = json.loads(signer.verify(token))
except (NotJwtError, ValueError):
return None
if not isinstance(claims, dict) or not isinstance(claims.get("exp"), int):
return None
if claims["exp"] <= time.time():
return None
return claims
Important differences vs JWT
- Not RFC 7519 JWT format (
header.payload.signature). - No
alg/kidheaders. - No built-in claim parsing (
exp,aud,iss) or automatic expiry checks. - Not intended for third-party JWT interoperability.
If you need standards-based interoperability, keep using a full JWT library.
Token format
Tokens are interchangeable with every not-jwt implementation. See the format specification.
Security notes
- Use a strong random secret key.
- Rotate keys when needed.
- Treat verify failures as authentication failures.
- Treat this as message integrity, not encryption.
Development
cd python
PYTHONPATH=src python -m unittest discover -s tests
Release files for not-jwt 1.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 | |
|---|---|---|---|
| not_jwt-1.1.0.tar.gz | 4.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| not_jwt-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 9.5 kB
Release files / not_jwt-1.1.0.tar.gz
| Download URL | not_jwt-1.1.0.tar.gz |
|---|---|
| Size | 4.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cead97f7b3748b4c18c5033695a3a6a788b1e3e2b11d76eb432c51ace3df2ab3
|
|
BLAKE2b-256 checksum How to use checksums |
8de3b791beda66259317e8a7baa983e1c6da463a3a0de45a7c3a1cb8c7d62738
|
| 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 25, 2026.
Transparency logRelease files / not_jwt-1.1.0-py3-none-any.whl
| Download URL | not_jwt-1.1.0-py3-none-any.whl |
|---|---|
| Size | 4.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e37ee23479f3915981f5357e2e8aa199e722ad9c796ad212c202f92896af8309
|
|
BLAKE2b-256 checksum How to use checksums |
56061fcb3f822f91ba661db4898e994d8a3f7c64e2ca843f2331e134340c8622
|
| 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 25, 2026.
Transparency log