mrkeyoor.com_
Wed 05 Aug 05:03 UTC
PyPISecurityupdated 05 Aug 2026

pyjwt

PyJWT is the standard Python implementation of JSON Web Tokens (RFC 7519). It encodes and decodes the compact signed-token format, verifying signatures and registered claims like exp, aud, and iss. It is a focused signing and verification library, not an auth framework: sessions, OAuth flows, and user models are your problem. Symmetric HS256 works out of the box; RSA and ECDSA algorithms need the optional cryptography dependency.

Verdict

The correct default for signing and verifying JWTs in Python. The moment you need encryption, key management, or login flows, step up to joserfc or Authlib instead of bolting extras onto PyJWT.

API stability5/5The 2.x API has been stable for years; the notable breaking change was the 1.x to 2.x tightening (string output from encode, mandatory algorithms on decode), which was a security improvement.
Docs4/5readthedocs covers usage, algorithm options, and JWKS with runnable examples; it is short, but so is the library surface, and the README quickstart is enough for the common case.
Maintenance4/5Steady releases under jpadilla with pushes as recent as 2026-08-03, plus a CI badge and codecov; it is a small-maintainer project rather than a company-backed one, hence not a 5.
Ecosystem5/5At roughly 166M weekly downloads it is the assumed JWT layer across Python web frameworks and auth providers' Python examples.

Use it if

  • You issue or verify JWTs in a Python API and want the boring, widely audited default that most frameworks build on
  • You verify RS256/ES256 tokens against a JWKS endpoint (Auth0, Cognito, Entra ID); PyJWKClient handles key fetching and caching
  • You want a light dependency: the base install is pure Python, and cryptography is only pulled in when you opt into asymmetric algorithms
Skip it if

Setup reality

pip install pyjwt covers HS256 end to end. The classic trip-ups: RS256 and friends raise at runtime until you install the pyjwt[crypto] extra, jwt.decode in 2.x demands an explicit algorithms list (code ported from the 1.x era breaks right there), and there is an unrelated package literally named jwt on PyPI that also imports as jwt, so one typo in requirements gets you a different library with a colliding module name.

Patterns

Sign a token with HS256encode-hs256

import jwt

token = jwt.encode({"sub": "user-123"}, "secret", algorithm="HS256")
# 'eyJhbGciOiJIUzI1NiIs...'

encode returns a str in 2.x (it was bytes in 1.x); use a long random secret, not a password.

Decode and verify a tokendecode-verify

import jwt

payload = jwt.decode(token, "secret", algorithms=["HS256"])

algorithms is mandatory in 2.x and must be a fixed allowlist; never derive it from the token header, that is the classic algorithm-confusion attack.

Issue and check expiring tokensexpiring-tokens

import jwt
from datetime import datetime, timedelta, timezone

now = datetime.now(timezone.utc)
token = jwt.encode(
    {"sub": "user-123", "iat": now, "exp": now + timedelta(minutes=15)},
    "secret",
    algorithm="HS256",
)

try:
    jwt.decode(token, "secret", algorithms=["HS256"])
except jwt.ExpiredSignatureError:
    ...  # force re-auth

datetime objects are converted to epoch seconds for you; exp is only enforced if the claim is present in the token.

Verify audience and issueraudience-issuer

payload = jwt.decode(
    token,
    "secret",
    algorithms=["HS256"],
    audience="my-api",
    issuer="https://auth.example.com",
)

If the token carries an aud claim and you pass no audience, decode raises InvalidAudienceError rather than silently passing.

Sign and verify with RS256rs256-keypair

import jwt

with open("private.pem") as f:
    private_key = f.read()
with open("public.pem") as f:
    public_key = f.read()

token = jwt.encode({"sub": "user-123"}, private_key, algorithm="RS256")
payload = jwt.decode(token, public_key, algorithms=["RS256"])

Requires pip install pyjwt[crypto]; without the extra you get an ImportError-flavored error only when an RSA algorithm is first used.

Verify against a JWKS endpoint (OIDC)jwks-verify

import jwt
from jwt import PyJWKClient

jwks_client = PyJWKClient("https://example.auth0.com/.well-known/jwks.json")

signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
    token,
    signing_key.key,
    algorithms=["RS256"],
    audience="my-api",
)

Construct PyJWKClient once at module or app scope; it caches keys, and rebuilding it per request refetches the JWKS every time.

Tolerate clock skewclock-skew-leeway

payload = jwt.decode(
    token,
    "secret",
    algorithms=["HS256"],
    leeway=10,  # seconds
)

leeway applies to exp, nbf, and iat checks; a few seconds covers real-world clock drift between servers.

Require specific claims to existrequire-claims

payload = jwt.decode(
    token,
    "secret",
    algorithms=["HS256"],
    options={"require": ["exp", "sub"]},
)

require only checks presence, not validity; a token with exp still needs the normal expiry check, which decode does anyway.

Read the header to pick a keyunverified-header

import jwt

header = jwt.get_unverified_header(token)
key = key_store[header["kid"]]
payload = jwt.decode(token, key, algorithms=["RS256"])

The header is attacker-controlled until the signature is verified; use it only for key lookup, never for trust decisions.

Catch token errors broadlyhandle-errors

import jwt

try:
    payload = jwt.decode(token, "secret", algorithms=["HS256"])
except jwt.ExpiredSignatureError:
    ...  # expired: offer refresh
except jwt.InvalidTokenError:
    ...  # anything else invalid: reject

InvalidTokenError is the base class for the whole family (signature, audience, issuer, decode errors), so order the except blocks specific-first.

Alternatives

PackageRegistryPick it when
joserfcPyPIFull JOSE support including JWE, when signed-only tokens stop being enough
authlibPyPIOAuth and OIDC client/server flows with JOSE built in
python-josePyPIA JOSE-shaped API some older stacks standardized on; check its maintenance status before adopting