Decode a JWT in Python with PyJWT
In Python projects, PyJWT is the standard library for JWT decoding and verification, with hundreds of millions of monthly PyPI downloads. Version 2.13.0, released May 2026, is the current stable release. A basic install: pip install PyJWT: has no required runtime dependencies for HMAC algorithms, making it lightweight for decode-only use cases.1 Paste any token into the decoder above for instant offline inspection without writing a single line of Python, or follow the patterns below to integrate token reading into your application.
The safer choice for server-side token handling is always offline browser inspection during debugging. Pasting a JWT into a server-backed tool transmits it to a third party. Because the decoder above runs entirely in your browser, your production tokens never leave your machine. For application code, PyJWT handles both the decode-without-verify pattern for debugging and full JWKS-based verification for production: you choose which mode to call based on whether you need security guarantees.
Library and setup
Install PyJWT with pip install PyJWT. For full JWKS-based signature verification, add the cryptography extra: pip install PyJWT[cryptography]. The cryptography package provides the RSA and EC key handling that RS256 and ES256 require. PyJWT follows semantic versioning: pin to a major version (PyJWT>=2,<3) in your requirements.txt to avoid breaking changes. Importing the library requires import jwt; no submodule import is needed for basic use. The jwt namespace provides all the functions you need for both decode and verify operations, including decode, get_unverified_header, and the PyJWKClient class that handles JWKS fetching and key rotation automatically.2
Keeping decode and verify separate
Use decode-only calls when you are diagnosing a token shape in development. Use verified decode in middleware, background jobs, and any code that grants access. The same library supports both modes, but the security boundary changes as soon as you remove verify_signature. A decode-only call in production middleware that grants access based on unverified claims effectively turns your API into an open endpoint, because any client can craft a JWT with arbitrary sub, iss, and role values that your server would accept without checking the cryptographic proof.
Decoding without verification
Reading a JWT payload without checking the signature uses jwt.decode() with a special options dict. Calling jwt.decode(token, options={'verify_signature': False}) returns the payload as a Python dict with all claims accessible by key. To read only the header before decoding the payload, call jwt.get_unverified_header(token): it returns the alg, kid, and typ fields without touching the payload.2 Consequently, reading the kid first lets you select the correct verification key from a JWKS before performing the full decode and verify call, which is the recommended two-step pattern when your middleware needs to log the algorithm and key ID before committing to the more expensive cryptographic verification step.
Reading specific claims
After decoding, access claims by key like any Python dict: payload['sub'] for the subject, payload.get('exp') for expiry, payload.get('email') for the email claim. Using .get() instead of direct key access avoids KeyError when a claim is absent, which is especially important for optional claims like email_verified or azp that some providers omit entirely. Furthermore, PyJWT's verify path auto-converts exp to a datetime and raises DecodeError if the token is expired: you get claim reading and expiry validation in one call. For custom or provider-specific claims, access them directly from the payload dict using whatever key the issuer used, always wrapping the access in a .get() call or a try/except block so that a missing custom key does not crash your request handler.
Full JWKS-based verification with PyJWT
For full RS256 verification using a JWKS endpoint, install PyJWT with the cryptography extra and use the built-in PyJWKClient class. Instantiate the client with your provider's JWKS URI, then call get_signing_key_from_jwt(token) to retrieve the matching signing key. Pass signing_key.key to jwt.decode() alongside your expected algorithm list, audience, and issuer to perform complete verification, which gives you cryptographic proof that the token was actually issued by your configured provider rather than constructed by an attacker.
ES256 and JWKS cache refresh
PyJWKClient caches the JWKS response and refreshes it automatically when it encounters an unknown kid in a token header.3 For ES256 tokens from providers that use ECDSA signing, change the algorithms parameter from RS256 to ES256; the same PyJWKClient approach handles key selection and caching without further code changes. ES256 signatures are substantially smaller than RS256 at equivalent security levels, which reduces token size and improves throughput in high-volume APIs that process thousands of authenticated requests per second, making it the preferred choice for latency-sensitive microservice architectures.4
Handling PyJWT exceptions in production code
Because PyJWT raises distinct exception classes for each failure mode, your exception handler can return the correct HTTP status for each case.5 Catching jwt.ExpiredSignatureError signals an expired token: return a 401 response and prompt the client to refresh. Catching jwt.InvalidAudienceError signals an aud mismatch: return a 403. Catching jwt.DecodeError covers malformed tokens that are not valid JWTs: return a 400 to indicate a client-side formatting issue. Mapping each exception to a specific HTTP status code ensures that well-behaved clients can implement the correct recovery strategy automatically rather than treating every 401 as a permanent failure.
Logging exception context without raw token data
Catch all three exception types separately rather than using a broad except Exception block. Separate handlers let you log each type at the correct severity: expired tokens are normal and expected; malformed tokens at high volume may indicate a client bug or a probing attempt. Log the exception class and the token's iss and sub alongside the error to aid debugging without logging the full raw token string, which keeps your log storage compliant with data-retention policies that restrict how long credential-adjacent data can persist.
Separating the exception types also helps your monitoring system alert on the right severity without manual triage. Expired tokens are routine and need no page, while a sudden spike in malformed tokens can indicate an attacker probing your endpoints. Before you add that logging, paste the failing token into the decoder for PyJWT exception handling patterns, to see exactly which claim triggered the exception, without writing it to a log file at all. Keeping the raw token out of the log line protects users and satisfies auditors who review what your systems retain.
When to use this
Use PyJWT when your Python backend needs to read or verify JWTs from any provider. The decode-without-verification pattern suits debugging and testing; the full verify path suits middleware that protects API routes.
Examples
Decode JWT payload without signature verification
import jwt
token = "eyJ..."
payload = jwt.decode(
token,
options={"verify_signature": False}
)
print(payload["sub"])
print(payload.get("email")) Useful for reading claims during development or test setup without needing the signing key.
Read only the JWT header (no payload decode)
import jwt
token = "eyJ..."
header = jwt.get_unverified_header(token)
print(header["alg"]) # e.g. RS256
print(header.get("kid")) # key ID for JWKS lookup Use get_unverified_header to select the correct JWKS key before full verification.
- 1.
PyPI, "PyJWT 2.13.0," pypi.org, May 2026. https://pypi.org/project/PyJWT/
- 2.
PyJWT, "API Reference," pyjwt.readthedocs.io, accessed June 2026. https://pyjwt.readthedocs.io/en/latest/api.html
- 3.
jpadilla/pyjwt, "jwt/jwks_client.py," github.com, accessed June 2026. https://github.com/jpadilla/pyjwt/blob/master/jwt/jwks_client.py
- 4.
M. Jones, "JSON Web Algorithms (JWA)," RFC 7518, IETF, May 2015. https://datatracker.ietf.org/doc/html/rfc7518
- 5.
jpadilla/pyjwt, "jwt/exceptions.py," github.com, accessed June 2026. https://github.com/jpadilla/pyjwt/blob/master/jwt/exceptions.py