JWT Decoder Python β Decode JWTs with PyJWT Guide
Use the free online JWT Decoder directly in your browser β no install required.
Try JWT Decoder Online βEvery API that uses token-based authentication hands you a JWTat some point, and figuring out what's inside it is one of those tasks that comes up constantly during development. A JWT decoder in Python takes that opaque base64 string and turns it into a readable claims dictionary you can actually work with. The PyPI package you want is PyJWT β installed with pip install PyJWT but imported as import jwt. This guide walks through jwt.decode() with full signature verification, decoding without a secret for quick inspection, manual base64 decoding without any library, RS256 public key verification, and common pitfalls I've hit in production auth systems. For a quick one-off check, the online JWT Decoder does this instantly without any code. All examples target Python 3.10+ and PyJWT 2.x.
- βpip install PyJWT, then import jwt β the package name and import name are different, which trips up almost everyone.
- βjwt.decode(token, key, algorithms=["HS256"]) returns a plain dict with the claims. Always pass algorithms explicitly.
- βTo inspect claims without verification: jwt.decode(token, options={"verify_signature": False}, algorithms=["HS256"]).
- βFor RSA/EC tokens: pip install PyJWT cryptography β the cryptography backend is required for asymmetric algorithms.
- βManual decoding (base64 + json) works without any library but skips all signature and expiry validation.
What is JWT Decoding?
A JSON Web Token is three base64url-encoded segments separated by dots: a header (algorithm and token type), a payload (the claims β user ID, roles, expiration time), and a signature. Decoding a JWT means extracting the header and payload segments, base64url-decoding them, and parsing the resulting JSON into a dictionary of claims.
The header tells you which algorithm was used to sign the token and sometimes includes a kid (key ID) for finding the right verification key. The payload holds the actual data: who the token was issued to (sub), when it expires (exp), which service it's intended for (aud), plus any custom claims your application defines. The signature segment proves the token was not tampered with, but you need the secret key or public key to verify it. Decoding and verification are separate operations. You can decode the payload without verifying the signature (useful for debugging), but never trust unverified claims for authorization decisions.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxMTgxNTYwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
{
"sub": "usr_8f2a",
"role": "admin",
"exp": 1711815600
}jwt.decode() β Decode and Verify with PyJWT
jwt.decode() is the primary function from the PyJWT library. It takes the encoded token string, the secret key (for HMAC algorithms) or public key (for RSA/EC), and a mandatory algorithms list. The function verifies the signature, checks standard claims like exp and nbf, and returns the payload as a Python dictionary. If anything fails β bad signature, expired token, wrong algorithm β it raises a specific exception.
Minimal Working Example
import jwt
# A shared secret between the issuer and this service
SECRET_KEY = "k8s-webhook-signing-secret-2026"
# Encode a token first (simulating what an auth server would issue)
token = jwt.encode(
{"sub": "usr_8f2a", "role": "admin", "team": "platform"},
SECRET_KEY,
algorithm="HS256"
)
print(token)
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3Jf...
# Decode and verify the token
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
print(payload)
# {'sub': 'usr_8f2a', 'role': 'admin', 'team': 'platform'}
print(payload["role"])
# adminThe algorithms parameter is a list, not a single string, and it is mandatory in PyJWT 2.x. This is a security feature: without it, an attacker could craft a token with alg: none in the header and bypass verification entirely. Always specify exactly which algorithms your application accepts. If you only issue HS256 tokens, the list should be ["HS256"] β not ["HS256", "RS256", "none"]. Keeping the list tight reduces the attack surface.
One thing that confused me early on: PyJWT 2.x changed jwt.encode() to return a string instead of bytes. If you're reading old Stack Overflow answers that call .decode("utf-8") on the encoded token, that code is from the PyJWT 1.x era and will throw an AttributeError on version 2.x. The token is already a string β just use it directly.
Full Round-Trip with Expiration
import jwt
from datetime import datetime, timedelta, timezone
SECRET_KEY = "webhook-processor-secret"
# Create a token that expires in 1 hour
payload = {
"sub": "svc_payment_processor",
"iss": "auth.internal.example.com",
"aud": "https://api.example.com",
"exp": datetime.now(timezone.utc) + timedelta(hours=1),
"permissions": ["orders:read", "refunds:create"],
}
token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
# Later, when the token arrives in a request header:
try:
decoded = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"],
audience="https://api.example.com",
issuer="auth.internal.example.com",
)
print(f"Service: {decoded['sub']}")
print(f"Permissions: {decoded['permissions']}")
except jwt.ExpiredSignatureError:
print("Token expired β request re-authentication")
except jwt.InvalidAudienceError:
print("Token not intended for this API")
except jwt.InvalidIssuerError:
print("Token issued by unknown authority")datetime objects to Unix timestamps automatically during encoding. During decoding, the exp, iat, and nbf claims come back as integers, not datetime objects. You need to convert them yourself with datetime.fromtimestamp(payload["exp"], tz=timezone.utc).Decode a JWT Without Signature Verification
Sometimes you need to read the claims before you can verify the token. A common scenario: the token header contains a kid (key ID) field, and you need to fetch the matching public key from a JWKS endpoint before you can verify. PyJWT supports this with the verify_signature: False option. You still pass the algorithms list, but the key argument is ignored.
import jwt
token = (
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpZy0xNzI2In0"
".eyJzdWIiOiJ1c3JfM2M3ZiIsInNjb3BlIjoicmVhZDpvcmRlcnMiLCJpc3MiOiJhdXRoLmV4YW1wbGUuY29tIn0"
".signature_placeholder"
)
# Step 1: Read claims without verification to get routing info
unverified = jwt.decode(
token,
options={"verify_signature": False},
algorithms=["RS256"]
)
print(unverified)
# {'sub': 'usr_3c7f', 'scope': 'read:orders', 'iss': 'auth.example.com'}
# Step 2: Read the header to find which key to use
header = jwt.get_unverified_header(token)
print(header)
# {'alg': 'RS256', 'typ': 'JWT', 'kid': 'sig-1726'}
# Now use header['kid'] to fetch the correct public key from your JWKS endpointThere's a subtle distinction here. jwt.get_unverified_header() only reads the header β the first segment. The jwt.decode() call with verify_signature: False reads the payload (second segment). Between the two, you can extract everything from a token without a key. PyJWT still validates that the token has the right structure (three dot-separated segments, valid base64, valid JSON) even when signature verification is off. If the token is structurally malformed, it raises DecodeError regardless of the options you pass.
jwt.decode() Parameters Reference
The full signature is jwt.decode(jwt, key, algorithms, options, audience, issuer, leeway, require). All parameters after algorithms are keyword-only.
The options dict gives fine-grained control over which validations PyJWT performs. The keys map to individual checks: verify_signature, verify_exp, verify_nbf, verify_iss, verify_aud, and verify_iat. All default to True except when you explicitly set them to False. In production, leave all of these at their defaults. The only time I disable individual checks is during development when I'm working with stale test tokens and need to bypass expiry temporarily.
import jwt
# Require specific claims to be present β raises MissingRequiredClaimError if absent
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"],
options={"require": ["exp", "iss", "sub"]},
issuer="auth.internal.example.com",
)
# During development only: skip expiry to test with old tokens
dev_payload = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"],
options={"verify_exp": False}, # DO NOT use in production
)Manual JWT Decoding with base64 and json
You can decode a JWT payload using nothing but the Python standard library β no pip install needed. This is genuinely useful in several situations: debugging scripts where adding a dependency is overkill, restricted CI environments where only the standard library is available, AWS Lambda functions where you want to minimize cold start time, or simply understanding what a JWT actually is under the hood. The process is straightforward: split on dots, take the segment you want, add base64 padding, decode, and parse the JSON.
The base64 and json modules are both in the Python standard library, so this approach works on any Python installation from 3.6 onwards. The functions below handle both the header (first segment) and payload (second segment) separately:
import base64
import json
def decode_jwt_payload(token: str) -> dict:
"""Decode the JWT payload without signature verification.
Works with any JWT β HS256, RS256, ES256, etc.
"""
parts = token.split(".")
if len(parts) != 3:
raise ValueError(f"Expected 3 JWT segments, got {len(parts)}")
payload_b64 = parts[1]
# base64url uses - and _ instead of + and /
# Python's urlsafe_b64decode handles this, but needs padding
payload_b64 += "=" * (-len(payload_b64) % 4)
payload_bytes = base64.urlsafe_b64decode(payload_b64)
return json.loads(payload_bytes)
def decode_jwt_header(token: str) -> dict:
"""Decode the JWT header (algorithm, key ID, type)."""
header_b64 = token.split(".")[0]
header_b64 += "=" * (-len(header_b64) % 4)
return json.loads(base64.urlsafe_b64decode(header_b64))
# Example usage
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcxMTgxNTYwMH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
header = decode_jwt_header(token)
print(f"Algorithm: {header['alg']}")
# Algorithm: HS256
claims = decode_jwt_payload(token)
print(f"Subject: {claims['sub']}")
print(f"Role: {claims['role']}")
# Subject: usr_8f2a
# Role: adminThe padding trick (+= "=" * (-len(s) % 4)) is the piece that everyone forgets. JWT base64url omits trailing = characters, but Python's urlsafe_b64decode requires them. Without the padding fix, you get a binascii.Error: Incorrect padding.
jwt.decode() with a real key.Decode JWTs from API Responses and Token Files
The two most common real-world scenarios: extracting a JWT from an HTTP response (an OAuth token endpoint, a login API), and reading tokens from files (service account credentials, Kubernetes mounted secrets, cached tokens on disk). Both need proper error handling. Network requests fail. Files go missing. Tokens expire between when they were cached and when they are read.
The examples below use httpx for HTTP calls (swap in requests if you prefer, the pattern is identical) and pathlib.Path for file operations. Each example catches specific PyJWT exceptions rather than a bare except Exception, so you can respond appropriately to each failure mode: re-authenticate on expiry, alert on signature failure, retry on network timeout.
Decode a JWT from an API Response
import jwt
import httpx # or requests
TOKEN_ENDPOINT = "https://auth.example.com/oauth/token"
SECRET_KEY = "shared-webhook-signing-key"
def get_and_decode_token() -> dict:
"""Fetch an access token from the auth server and decode it."""
try:
response = httpx.post(
TOKEN_ENDPOINT,
data={
"grant_type": "client_credentials",
"client_id": "svc_order_processor",
"client_secret": "cs_9f3a7b2e",
},
timeout=10.0,
)
response.raise_for_status()
except httpx.HTTPError as exc:
raise RuntimeError(f"Token request failed: {exc}") from exc
token_data = response.json()
access_token = token_data["access_token"]
try:
payload = jwt.decode(
access_token,
SECRET_KEY,
algorithms=["HS256"],
audience="https://api.example.com",
)
return payload
except jwt.InvalidTokenError as exc:
raise RuntimeError(f"Invalid token from auth server: {exc}") from exc
claims = get_and_decode_token()
print(f"Service: {claims['sub']}, Scopes: {claims.get('scope', 'none')}")Decode a JWT from a File
import jwt
from pathlib import Path
from datetime import datetime, timezone
TOKEN_PATH = Path("/var/run/secrets/service-account-token")
PUBLIC_KEY_PATH = Path("/etc/ssl/auth/public_key.pem")
def decode_token_from_file() -> dict:
"""Read a JWT from a file, verify with a PEM public key."""
try:
token = TOKEN_PATH.read_text().strip()
public_key = PUBLIC_KEY_PATH.read_text()
except FileNotFoundError as exc:
raise RuntimeError(f"Missing file: {exc.filename}") from exc
try:
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="https://internal-api.example.com",
)
except jwt.ExpiredSignatureError:
exp_time = jwt.decode(
token,
options={"verify_signature": False},
algorithms=["RS256"],
).get("exp", 0)
expired_at = datetime.fromtimestamp(exp_time, tz=timezone.utc)
raise RuntimeError(f"Token expired at {expired_at.isoformat()}")
except jwt.InvalidTokenError as exc:
raise RuntimeError(f"Token verification failed: {exc}") from exc
return payload
claims = decode_token_from_file()
print(f"Subject: {claims['sub']}, Issuer: {claims['iss']}")Command-Line JWT Decoding
Sometimes you just need to peek at a token from the terminal without writing a script. Maybe you are debugging an OAuth flow and want to see what's in the Authorization header, or you grabbed a token from browser DevTools and want to check its expiry. Python's -c flag makes this a one-liner. Pipe in the token and get the claims as formatted JSON. No script file needed, no virtual environment.
# Decode JWT payload from clipboard or variable (no verification)
echo "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiJ9.sig" \
| python3 -c "
import sys, base64, json
token = sys.stdin.read().strip()
payload = token.split('.')[1]
payload += '=' * (-len(payload) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2))
"
# {
# "sub": "usr_8f2a",
# "role": "admin"
# }# Decode JWT header to check algorithm and key ID
echo "eyJhbGciOiJSUzI1NiIsImtpZCI6InNpZy0xNzI2In0.payload.sig" \
| python3 -c "
import sys, base64, json
token = sys.stdin.read().strip()
header = token.split('.')[0]
header += '=' * (-len(header) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(header)), indent=2))
"
# {
# "alg": "RS256",
# "kid": "sig-1726"
# }# If PyJWT is installed, verify and decode in one step
python3 -c "
import jwt, sys, json
token = sys.argv[1]
payload = jwt.decode(token, options={'verify_signature': False}, algorithms=['HS256'])
print(json.dumps(payload, indent=2))
" "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSJ9.sig"For a visual alternative without any terminal setup, paste your token into the ToolDeck JWT Decoder and see the header, payload, and signature verification status instantly.
python-jose and Other Alternatives
python-jose is an alternative JWT library that supports JWS, JWE (encrypted tokens), and JWK natively. If your application needs to handle encrypted JWTs (JWE) β where the payload itself is encrypted, not just signed β python-jose is the right pick because PyJWT does not support JWE at all. The library also has built-in JWKS key set handling, which simplifies integration with identity providers like Auth0, Okta, or Keycloak that expose rotating key sets. The decode interface is nearly identical to PyJWT, so switching between them requires minimal code changes:
# pip install python-jose[cryptography]
from jose import jwt as jose_jwt
token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInNjb3BlIjoib3JkZXJzOnJlYWQifQ.signature"
# Verified decode β same pattern as PyJWT
payload = jose_jwt.decode(
token,
"signing-secret-key",
algorithms=["HS256"],
audience="https://api.example.com",
)
print(payload)
# {'sub': 'usr_8f2a', 'scope': 'orders:read'}
# Unverified decode
claims = jose_jwt.get_unverified_claims(token)
header = jose_jwt.get_unverified_header(token)
print(f"Algorithm: {header['alg']}, Subject: {claims['sub']}")My recommendation: start with PyJWT. It covers 95% of JWT use cases, has the largest community, and the API is clean. Switch to python-jose if you need JWE support or prefer its JWKS handling. A third option worth mentioning is Authlib, which bundles JWT handling inside a much larger OAuth/OIDC framework. If you are already using Authlib for OAuth client flows, its authlib.jose.jwt module saves you from adding a second JWT dependency. Otherwise, it is a heavy dependency just for token decoding.
Terminal Output with Syntax Highlighting
Reading raw JWT claims in a terminal is fine for quick checks, but when you are debugging token payloads regularly (I did this daily while building an internal auth gateway), colorized output makes a real difference. String values, numbers, booleans, and null all render in distinct colors, which means you can spot a missing permission or wrong expiry timestamp at a glance without reading every character.
The rich library (pip install rich) has a print_json function that takes either a JSON string or a Python dict and prints it with full syntax highlighting to the terminal. Combine it with PyJWT for a two-line JWT inspection workflow:
# pip install rich PyJWT
import jwt
from rich import print_json
token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiIsInBlcm1pc3Npb25zIjpbIm9yZGVyczpyZWFkIiwicmVmdW5kczpjcmVhdGUiXSwiZXhwIjoxNzExODE1NjAwfQ.sig"
payload = jwt.decode(
token,
options={"verify_signature": False},
algorithms=["HS256"]
)
# Colorized, indented JSON output in the terminal
print_json(data=payload)
# {
# "sub": "usr_8f2a", β strings in green
# "role": "admin",
# "permissions": [
# "orders:read",
# "refunds:create"
# ],
# "exp": 1711815600 β numbers in cyan
# }rich output contains ANSI escape codes. Do not write it to files or return it from API endpoints β it is for terminal display only. Use json.dumps() when you need plain text output.Working with Large Token Batches
JWT tokens themselves are small (typically under 2 KB each), but there are scenarios where you process them in bulk. Audit log analysis after a security incident. Session migration scripts when switching auth providers. Compliance batch validation where you need to prove every token issued in the last 90 days was signed with the correct key. If you have tens of thousands of tokens in an NDJSON log file, processing them line-by-line avoids loading the entire file into memory and lets you report results incrementally.
Batch Validate Tokens from an Audit Log
import jwt
import json
from pathlib import Path
SECRET_KEY = "audit-log-signing-key"
def validate_token_log(log_path: str) -> dict:
"""Process an NDJSON file where each line has a 'token' field.
Returns counts of valid, expired, and invalid tokens.
"""
stats = {"valid": 0, "expired": 0, "invalid": 0}
with open(log_path) as fh:
for line_num, line in enumerate(fh, 1):
line = line.strip()
if not line:
continue
try:
record = json.loads(line)
token = record["token"]
except (json.JSONDecodeError, KeyError):
stats["invalid"] += 1
continue
try:
jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
stats["valid"] += 1
except jwt.ExpiredSignatureError:
stats["expired"] += 1
except jwt.InvalidTokenError:
stats["invalid"] += 1
return stats
result = validate_token_log("auth-events-2026-03.ndjson")
print(f"Valid: {result['valid']}, Expired: {result['expired']}, Invalid: {result['invalid']}")
# Valid: 14832, Expired: 291, Invalid: 17Extract Claims from NDJSON Token Export
import base64
import json
from datetime import datetime, timezone
def extract_claims_stream(input_path: str, output_path: str):
"""Read tokens line by line, decode payloads, write flattened claims."""
with open(input_path) as infile, open(output_path, "w") as outfile:
for line in infile:
line = line.strip()
if not line:
continue
record = json.loads(line)
token = record.get("access_token", "")
parts = token.split(".")
if len(parts) != 3:
continue
payload_b64 = parts[1] + "=" * (-len(parts[1]) % 4)
claims = json.loads(base64.urlsafe_b64decode(payload_b64))
# Flatten into an audit-friendly record
flat = {
"timestamp": record.get("timestamp"),
"subject": claims.get("sub"),
"issuer": claims.get("iss"),
"expired_at": datetime.fromtimestamp(
claims.get("exp", 0), tz=timezone.utc
).isoformat(),
}
outfile.write(json.dumps(flat) + "\n")
extract_claims_stream("token-audit.ndjson", "claims-extract.ndjson")multiprocessing.Pool to distribute validation across cores, since each token is independent.Common Mistakes
These four mistakes come up repeatedly in code reviews and Stack Overflow questions. Each one is easy to make, and the error message PyJWT gives you does not always point directly at the cause. I have seen the package naming issue alone waste hours of debugging time when someone installs the wrong library and gets a completely unexpected API.
Problem: Running pip install jwt or pip install python-jwt installs a completely different package. import jwt then fails or gives you an API you don't recognize.
Fix: Always install with pip install PyJWT. The import is import jwt. Check with pip show PyJWT to confirm the right package.
pip install jwt # or pip install python-jwt import jwt # wrong package β different API entirely
pip install PyJWT import jwt # correct β this is PyJWT print(jwt.__version__) # 2.x
Problem: In PyJWT 1.x, algorithms was optional and defaulted to allowing any algorithm. This created a security vulnerability where an attacker could set alg: none. PyJWT 2.x now raises DecodeError if algorithms is missing.
Fix: Always pass algorithms as an explicit list. Use only the algorithms your application actually issues tokens with.
# PyJWT 2.x β this raises DecodeError payload = jwt.decode(token, SECRET_KEY) # DecodeError: algorithms must be specified
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
Problem: Passing a string secret to jwt.decode() with algorithms=["RS256"] raises InvalidSignatureError. RS256 requires a PEM-encoded public key, not a shared secret string.
Fix: Load the PEM public key from a file or environment variable. Install the cryptography package: pip install PyJWT cryptography.
# This fails β RS256 needs a public key, not a string secret payload = jwt.decode(token, "my-secret", algorithms=["RS256"]) # InvalidSignatureError
public_key = open("public_key.pem").read()
payload = jwt.decode(token, public_key, algorithms=["RS256"])Problem: JWT base64url encoding strips trailing = characters. Python's base64.urlsafe_b64decode raises binascii.Error: Incorrect padding if you pass the raw segment without fixing the padding.
Fix: Add padding before decoding: segment += '=' * (-len(segment) % 4). This formula always produces the correct number of padding characters (0, 1, 2, or 3).
import base64
payload_b64 = token.split(".")[1]
data = base64.urlsafe_b64decode(payload_b64)
# binascii.Error: Incorrect paddingimport base64
payload_b64 = token.split(".")[1]
payload_b64 += "=" * (-len(payload_b64) % 4)
data = base64.urlsafe_b64decode(payload_b64) # worksPyJWT vs Alternatives β Quick Comparison
PyJWT is the right starting point for most Python applications. It covers HMAC and (with the cryptography backend) RSA and EC signature verification. If you need JWE (encrypted tokens), switch to python-jose or Authlib. Manual base64 decoding works for debugging but provides zero safety guarantees.
Here is when I reach for each option: PyJWT for any standard web service doing HS256 or RS256 verification. python-jose when the architecture includes encrypted tokens or JWKS rotation. Manual base64 for quick inspection in environments where pip is unavailable (CI containers, restricted production hosts, AWS Lambda cold starts where you want to minimize dependencies). Authlib when the project already uses it for OAuth client flows and adding another JWT library would be redundant.
For a no-code alternative, paste any token into the JWT Decoder to see the decoded header and payload with claim validation feedback.
Frequently Asked Questions
How do I decode a JWT in Python without verifying the signature?
Pass options={"verify_signature": False} and algorithms=["HS256"] to jwt.decode(). This returns the payload dict without checking the signature. Use this only for inspection β reading claims before fetching the right public key, or debugging in development. Never skip verification on tokens that gate access to anything.
import jwt
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c3JfOGYyYSIsInJvbGUiOiJhZG1pbiJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
payload = jwt.decode(
token,
options={"verify_signature": False},
algorithms=["HS256"]
)
print(payload)
# {'sub': 'usr_8f2a', 'role': 'admin'}What is the difference between PyJWT and python-jwt?
PyJWT (pip install PyJWT, import jwt) is the most popular JWT library for Python with over 80 million monthly downloads. python-jwt (pip install python_jwt) is a separate, much less used library with a different API surface. If you see import jwt in someone's code, they are using PyJWT. The naming confusion comes from the PyPI package name (PyJWT) differing from the import name (jwt). Stick with PyJWT unless you have a specific reason not to.
How do I decode a JWT with RS256 in Python?
Install both PyJWT and the cryptography backend: pip install PyJWT cryptography. Then pass the PEM-encoded public key as the key argument and algorithms=["RS256"]. PyJWT delegates the RSA signature verification to the cryptography library. Without the cryptography package installed, PyJWT raises an error when you try to use RSA or EC algorithms.
import jwt
public_key = open("public_key.pem").read()
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="https://api.example.com"
)Why does PyJWT raise ExpiredSignatureError?
PyJWT checks the exp (expiration) claim by default. If the current UTC time is past the exp timestamp, it raises jwt.ExpiredSignatureError. You can add clock skew tolerance with the leeway parameter: jwt.decode(token, key, algorithms=["HS256"], leeway=timedelta(seconds=30)). This gives a 30-second grace period. To disable expiry checking entirely (not recommended for production), pass options={"verify_exp": False}.
import jwt
from datetime import timedelta
try:
payload = jwt.decode(token, secret, algorithms=["HS256"], leeway=timedelta(seconds=30))
except jwt.ExpiredSignatureError:
print("Token has expired β prompt re-authentication")Can I read JWT claims without any library in Python?
Yes. Split the token on dots, take the second segment (the payload), pad it with = characters to make the length a multiple of 4, then base64url-decode it and parse the JSON. This gives you the claims dict but does not verify the signature. It is useful in restricted environments where you cannot install PyJWT, or for quick debugging scripts.
import base64, json
token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfOGYyYSJ9.signature"
payload_b64 = token.split(".")[1]
payload_b64 += "=" * (-len(payload_b64) % 4) # fix padding
claims = json.loads(base64.urlsafe_b64decode(payload_b64))
print(claims) # {'sub': 'usr_8f2a'}How do I validate the audience claim with PyJWT?
Pass the audience parameter to jwt.decode(): jwt.decode(token, key, algorithms=["HS256"], audience="https://api.example.com"). PyJWT compares the aud claim in the token against the value you provide. If the token has no aud claim, or the value does not match, it raises jwt.InvalidAudienceError. You can also pass a list of acceptable audiences if your service accepts tokens intended for multiple APIs.
import jwt
payload = jwt.decode(
token,
secret,
algorithms=["HS256"],
audience=["https://api.example.com", "https://admin.example.com"]
)Related Tools
Dmitri is a DevOps engineer who relies on Python as his primary scripting and automation language. He builds internal tooling, CI/CD pipelines, and infrastructure automation scripts that run in production across distributed teams. He writes about the Python standard library, subprocess management, file processing, encoding utilities, and the practical shell-adjacent Python that DevOps engineers use every day.
Maria is a backend developer specialising in Python and API integration. She has broad experience with data pipelines, serialisation formats, and building reliable server-side services. She is an active member of the Python community and enjoys writing practical, example-driven guides that help developers solve real problems without unnecessary theory.