Skip to main content

not-jwt (Python)

A tiny HMAC-SHA256 message signer for Python, with no dependencies. It gives you one simple contract:

  • sign(message) -> signed string
  • verify(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:

  1. Put claims into a JSON payload.
  2. Add exp (expiry) yourself.
  3. Sign the serialized payload.
  4. 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/kid headers.
  • 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)

Source distribution for not-jwt 1.1.0
File Size Uploaded
not_jwt-1.1.0.tar.gz 4.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for not-jwt 1.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page