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.0.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.0.0
File Size Uploaded
not_jwt-1.0.0.tar.gz 4.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for not-jwt 1.0.0
File Interpreter ABI Platform
not_jwt-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 9.5 kB

Release files / not_jwt-1.0.0.tar.gz

Download URL not_jwt-1.0.0.tar.gz
Size 4.9 kB
Tags Source
SHA-256 checksum
How to use checksums
3720dc155baf9bb7fbb8f2e31e31f22c22e38da9d648661f189cb347cf45952d
BLAKE2b-256 checksum
How to use checksums
321fea6547ca3a970b4ad4dc47b7a075540ede7a367ae419f3f3cddba59c33e6
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.0.0-py3-none-any.whl

Download URL not_jwt-1.0.0-py3-none-any.whl
Size 4.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b066d2c679b9f4068c04072622a125d6d54d032570a84b7206b2f9f67317b7d6
BLAKE2b-256 checksum
How to use checksums
dffa566422122162458f482c9f0c18d838b1e7cdb3931a7fe7cb90ed674fa565
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

1.1.0

2 release files

This release

1.0.0 This release

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