Python requests exceptions: full list and error handling (tested)
All requests exceptions inherit from requests.exceptions.RequestException. We printed the full hierarchy from requests 2.32.3, triggered each common one against a local server, and tested an except order that handles HTTP errors, timeouts, connection failures and bad JSON separately.
Setup: a small local test server (srv.py) with a 404 path, a path that sleeps 3 seconds, a path that redirects to itself, and an HTML page:
import http.server, threading, time
class H(http.server.BaseHTTPRequestHandler):
def log_message(self, *a): pass
def do_GET(self):
if self.path == "/slow":
time.sleep(3)
if self.path == "/loop":
self.send_response(302); self.send_header("Location", "/loop"); self.end_headers(); return
code = 404 if self.path == "/missing" else 200
self.send_response(code); self.send_header("Content-Type", "text/html"); self.end_headers()
self.wfile.write(b"<html>not json</html>")
def serve(port=8963):
srv = http.server.ThreadingHTTPServer(("localhost", port), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
1. The full list of requests exceptions
import inspect, requests.exceptions as rex
def name(c):
return c.__name__ if c.__module__ == "builtins" else f"{c.__module__}.{c.__name__}"
for n, cls in inspect.getmembers(rex, inspect.isclass):
if issubclass(cls, rex.RequestException):
print(f"{n:22} <- {', '.join(name(b) for b in cls.__bases__)}")
Output:
ChunkedEncodingError <- requests.exceptions.RequestException
ConnectTimeout <- requests.exceptions.ConnectionError, requests.exceptions.Timeout
ConnectionError <- requests.exceptions.RequestException
ContentDecodingError <- requests.exceptions.RequestException, urllib3.exceptions.HTTPError
HTTPError <- requests.exceptions.RequestException
InvalidHeader <- requests.exceptions.RequestException, ValueError
InvalidJSONError <- requests.exceptions.RequestException
InvalidProxyURL <- requests.exceptions.InvalidURL
InvalidSchema <- requests.exceptions.RequestException, ValueError
InvalidURL <- requests.exceptions.RequestException, ValueError
JSONDecodeError <- requests.exceptions.InvalidJSONError, json.decoder.JSONDecodeError
MissingSchema <- requests.exceptions.RequestException, ValueError
ProxyError <- requests.exceptions.ConnectionError
ReadTimeout <- requests.exceptions.Timeout
RequestException <- OSError
RetryError <- requests.exceptions.RequestException
SSLError <- requests.exceptions.ConnectionError
StreamConsumedError <- requests.exceptions.RequestException, TypeError
Timeout <- requests.exceptions.RequestException
TooManyRedirects <- requests.exceptions.RequestException
URLRequired <- requests.exceptions.RequestException
UnrewindableBodyError <- requests.exceptions.RequestException
Everything requests raises derives from RequestException, which itself is an OSError. The URL errors (MissingSchema, InvalidSchema, InvalidURL) are also ValueErrors, and JSONDecodeError is also a json.JSONDecodeError.
2. What raises which exception
import requests
from srv import serve
serve(8963)
tests = [("example.com/api", {}), ("ftp://example.com/", {}), ("http://", {}),
("http://localhost:8967/", {}), # nothing listening
("http://localhost:8963/slow", {"timeout": 1}),
("http://localhost:8963/loop", {}),
("https://localhost:8963/", {})] # https on a plain-HTTP port
for url, kw in tests:
try:
requests.get(url, **kw)
except requests.exceptions.RequestException as e:
print(f"{url}\n {type(e).__name__}: {str(e)[:150]}")
Output:
example.com/api
MissingSchema: Invalid URL 'example.com/api': No scheme supplied. Perhaps you meant https://example.com/api?
ftp://example.com/
InvalidSchema: No connection adapters were found for 'ftp://example.com/'
http://
InvalidURL: Invalid URL 'http://': No host supplied
http://localhost:8967/
ConnectionError: HTTPConnectionPool(host='localhost', port=8967): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPConnection ob
http://localhost:8963/slow
ReadTimeout: HTTPConnectionPool(host='localhost', port=8963): Read timed out. (read timeout=1)
http://localhost:8963/loop
TooManyRedirects: Exceeded 30 redirects.
https://localhost:8963/
SSLError: HTTPSConnectionPool(host='localhost', port=8963): Max retries exceeded with url: / (Caused by SSLError(SSLError(1, '[SSL: WRONG_VERSION_NUMBER] wrong
Messages are cut at 150 characters. ConnectionError (nothing listening) and SSLError share the long Max retries exceeded text; the real cause is in the Caused by part. A read timeout and a redirect loop each get their own class.
3. HTTP errors and bad JSON do not raise by themselves
import requests
from srv import serve
serve(8963)
r = requests.get("http://localhost:8963/missing", timeout=5)
print("no exception yet, status", r.status_code)
try:
r.raise_for_status()
except requests.exceptions.HTTPError as e:
print("HTTPError:", e, "| e.response.status_code =", e.response.status_code)
r = requests.get("http://localhost:8963/", timeout=5)
try:
r.json()
except requests.exceptions.JSONDecodeError as e:
print("JSONDecodeError:", e)
import json
print("also json.JSONDecodeError / ValueError:", isinstance(e, json.JSONDecodeError), isinstance(e, ValueError))
Output:
no exception yet, status 404
HTTPError: 404 Client Error: Not Found for url: http://localhost:8963/missing | e.response.status_code = 404
JSONDecodeError: Expecting value: line 1 column 1 (char 0)
also json.JSONDecodeError / ValueError: True True
A 404 or 500 is a successful request as far as requests is concerned: you get a response, no exception. Call r.raise_for_status() to turn 4xx/5xx into HTTPError; the response is on e.response. r.json() raises requests.exceptions.JSONDecodeError, which except ValueError and except json.JSONDecodeError also catch.
4. An error-handling pattern that covers everything
import requests
from srv import serve
serve(8963)
def fetch(url):
try:
r = requests.get(url, timeout=(3, 1)) # (connect, read) seconds
r.raise_for_status()
return r.json()
except requests.exceptions.HTTPError as e:
return f"HTTP {e.response.status_code}"
except requests.exceptions.Timeout:
return "timed out"
except requests.exceptions.ConnectionError:
return "could not connect"
except requests.exceptions.JSONDecodeError:
return "not JSON"
except requests.exceptions.RequestException as e: # anything else from requests
return f"request failed: {type(e).__name__}"
for path in ("http://localhost:8963/missing", "http://localhost:8963/slow",
"http://localhost:8967/", "http://localhost:8963/", "localhost:8963"):
print(f"{path:32} -> {fetch(path)}")
Output:
http://localhost:8963/missing -> HTTP 404
http://localhost:8963/slow -> timed out
http://localhost:8967/ -> could not connect
http://localhost:8963/ -> not JSON
localhost:8963 -> request failed: InvalidSchema
Order matters because of the hierarchy: put specific classes first and RequestException last. Timeout must come before ConnectionError if you want ConnectTimeout reported as a timeout, since it is both. Note the last line: "localhost:8963" without http:// raised InvalidSchema, not MissingSchema, because localhost was parsed as the scheme. Always set a timeout: without one, requests can wait forever on a server that stops responding.
5. requests.ConnectionError is not Python's ConnectionError
import builtins, requests
e = requests.exceptions.ConnectionError("x")
print("requests ConnectionError is builtin ConnectionError:", isinstance(e, builtins.ConnectionError))
print("requests ConnectionError is OSError:", isinstance(e, OSError))
print("ReadTimeout is ConnectionError:", issubclass(requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError))
print("ConnectTimeout is ConnectionError:", issubclass(requests.exceptions.ConnectTimeout, requests.exceptions.ConnectionError))
Output:
requests ConnectionError is builtin ConnectionError: False
requests ConnectionError is OSError: True
ReadTimeout is ConnectionError: False
ConnectTimeout is ConnectionError: True
except ConnectionError without the module prefix catches the built-in class, which requests does not raise; use requests.exceptions.ConnectionError (or requests.ConnectionError). ReadTimeout is not a ConnectionError, so a ConnectionError clause alone misses read timeouts.
Related: requests SSLError: HTTPSConnectionPool · ConnectionError: Max retries exceeded · ReadTimeout · get the message from an HTTPError.
Frequently asked questions
What is the base exception in Python requests?
requests.exceptions.RequestException (a subclass of OSError). Catching it catches every exception requests raises.
Does requests raise an exception for 404 or 500?
No. Call response.raise_for_status() to raise requests.exceptions.HTTPError for 4xx and 5xx responses.
Is requests.exceptions.ConnectionError the same as Python's ConnectionError?
No. It inherits from RequestException and OSError, not the built-in ConnectionError, so catch requests.exceptions.ConnectionError explicitly.
Written with AI assistance. Measured on 2026-10-23 with Python 3.13.5 and requests 2.32.3 against a local http.server. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; requests is a free third-party library.