urllib.error.URLError: urlopen error (tested)

urllib.error.URLError: <urlopen error ...> means urlopen never got an HTTP response. We reproduced the common causes on Python 3.13.5 and found two that don't behave the way the exception hierarchy suggests.

1. Read e.reason: it says what actually failed

import urllib.request, urllib.error
for url in ["http://nonexistent.invalid/", "http://127.0.0.1:1/"]:
    try:
        urllib.request.urlopen(url, timeout=3)
    except urllib.error.URLError as e:
        print(url)
        print("  ", type(e).__name__, "|", e)
        print("   reason:", type(e.reason).__name__, repr(e.reason))

Output:

http://nonexistent.invalid/
   URLError | <urlopen error [Errno -2] Name or service not known>
   reason: gaierror gaierror(-2, 'Name or service not known')
http://127.0.0.1:1/
   URLError | <urlopen error [Errno 111] Connection refused>
   reason: ConnectionRefusedError ConnectionRefusedError(111, 'Connection refused')

Measured on Python 3.13.5: URLError is a wrapper. The real error is in e.reason: a socket.gaierror for a hostname that doesn't resolve (typo, no DNS, no network), a ConnectionRefusedError when nothing listens on that port. See our Errno 111 guide for the refused case.

2. HTTPError is a URLError: catch it first

Blocks 2, 3 and 5 import this small local test server (port 8964): /missing returns 404 with a body, /slow waits 3 seconds before answering.

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)
        code = 404 if self.path == "/missing" else 200
        body = b"no such page" if code == 404 else b"ok"
        self.send_response(code); self.send_header("Content-Length", str(len(body))); self.end_headers()
        self.wfile.write(body)
def start():
    s = http.server.ThreadingHTTPServer(("127.0.0.1", 8964), H)
    threading.Thread(target=s.serve_forever, daemon=True).start()
import urllib.request, urllib.error
from server import start; start()
url = "http://127.0.0.1:8964/missing"
# Wrong order: the URLError branch also catches HTTPError
try:
    urllib.request.urlopen(url)
except urllib.error.URLError as e:
    print("caught as URLError:", type(e).__name__, e)
# Right order: HTTPError first
try:
    urllib.request.urlopen(url)
except urllib.error.HTTPError as e:
    print("HTTPError", e.code, e.read())
except urllib.error.URLError as e:
    print("network problem:", e.reason)

Output:

caught as URLError: HTTPError HTTP Error 404: Not Found
HTTPError 404 b'no such page'

Measured: HTTPError subclasses URLError, so an except URLError placed first also swallows every 404 and 500, and code that reports "network problem" is really looking at a server reply. Put except HTTPError above it; e.code and e.read() give the status and body. For 403s from sites that block Python's default User-Agent, see our urllib 403 guide.

3. A read timeout is not a URLError

import urllib.request, urllib.error
from server import start; start()
try:
    urllib.request.urlopen("http://127.0.0.1:8964/slow", timeout=1)
except urllib.error.URLError as e:
    print("URLError:", e)
except TimeoutError as e:
    print("not a URLError:", type(e).__name__, e)
    print("is OSError:", isinstance(e, OSError))

Output:

not a URLError: TimeoutError timed out
is OSError: True

Measured: the server accepted the connection, then took 3 seconds to answer. With timeout=1 that raised a bare TimeoutError, which an except URLError does not catch. A timeout while connecting is different: we pointed it at an unroutable address and got URLError: <urlopen error timed out> with reason a TimeoutError. Handle both.

4. "unknown url type": a missing scheme is a ValueError

import urllib.request
for url in ["example.com", "htps://example.com"]:
    try:
        urllib.request.urlopen(url, timeout=3)
    except Exception as e:
        print(repr(url), "->", type(e).__name__, e)

Output:

'example.com' -> ValueError unknown url type: 'example.com'
'htps://example.com' -> URLError <urlopen error unknown url type: htps>

Measured: a URL without http:// raises ValueError, not URLError. A misspelled scheme raises URLError. Both mean the URL string is wrong, not the network.

5. One fetch() that handles every case

import urllib.request, urllib.error
from server import start; start()

def fetch(url, timeout=1):
    try:
        with urllib.request.urlopen(url, timeout=timeout) as r:
            return f"{r.status} {r.read()!r}"
    except urllib.error.HTTPError as e:      # server answered with 4xx/5xx
        return f"HTTP {e.code}: {e.read()!r}"
    except urllib.error.URLError as e:       # DNS, refused, SSL, bad scheme
        return f"no response: {e.reason}"
    except TimeoutError:                     # timed out while reading
        return "timed out"

for path in ["/", "/missing", "/slow"]:
    print(path, "->", fetch("http://127.0.0.1:8964" + path))
print(fetch("http://127.0.0.1:1/"))
print(fetch("http://nonexistent.invalid/"))

Output:

/ -> 200 b'ok'
/missing -> HTTP 404: b'no such page'
/slow -> timed out
no response: [Errno 111] Connection refused
no response: [Errno -2] Name or service not known

Measured: HTTPError first, then URLError, then TimeoutError for slow reads. SSL failures also arrive as a URLError whose reason is an SSLCertVerificationError; see our CERTIFICATE_VERIFY_FAILED guide.

Frequently asked questions

What does urllib.error.URLError: urlopen error mean?

urlopen got no HTTP response at all. The cause is in e.reason; we measured a gaierror for an unresolvable host and ConnectionRefusedError for a closed port on Python 3.13.5.

What is the difference between URLError and HTTPError?

HTTPError means the server answered with an error status; URLError means no answer. HTTPError subclasses URLError, so we measured that an except URLError written first also catches 404s.

Why doesn't except URLError catch my urllib timeout?

A timeout while reading the response raises a bare TimeoutError. We measured that with a server that answered after 3 s and timeout=1; connect timeouts are wrapped in URLError.

What does urlopen error unknown url type mean?

The URL has a wrong scheme. We measured 'htps://' raising URLError and a URL with no scheme at all raising ValueError.

Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5: urlopen against an unresolvable host, a closed port, a local server returning 404 and a slow response, an unroutable address, and bad URL schemes. Every block was run verbatim and its output pasted from that run. PlainHeaders is our extension; everything else here is built-in Python.