# Python [SSL: WRONG_VERSION_NUMBER] wrong version number: fixes

> WRONG_VERSION_NUMBER means Python spoke TLS to something that answered in plain text: an https:// proxy URL, a wrong port, or a server without TLS on that port.

Source: https://ssl.sh/errors/python-ssl-wrong-version-number/
Last updated: 2026-11-10

**`[SSL: WRONG_VERSION_NUMBER] wrong version number` means Python started a TLS handshake, but the reply was not TLS at all.** The other side is speaking plain HTTP or another plaintext protocol on that port, most often because a proxy URL starts with `https://`, the code uses the wrong port, or the server is not running TLS where you expect it.

## Quick fix

An `https://` proxy URL or a plaintext port causes most cases.

Check what is listening on the host and port, and which proxy Python is using:

```bash
# Does the port answer plain HTTP?
curl -sv http://example.com:443/ -o /dev/null

# Which proxies will Python use?
python3 -c "import urllib.request; print(urllib.request.getproxies())"
env | grep -i _proxy
```

- If the proxy value starts with `https://`, change it to `http://` (the scheme describes how you talk to the proxy, not to the destination):

```bash
export HTTPS_PROXY=http://proxy.example.com:8080
export HTTP_PROXY=http://proxy.example.com:8080
```

- If `curl http://...:443/` returns an HTTP response, that port is serving plain text; fix the server or use the right port.
- For mail and other protocols, match the client class to the port (see below).

## Causes

### 1. An `https://` proxy URL for a plain HTTP proxy

An `https://` proxy URL for a plain HTTP proxy is the most common cause with requests, pip and other urllib3-based tools. Corporate proxies, debugging proxies (Fiddler, Charles, mitmproxy) and local proxy clients normally accept plain HTTP on their port and tunnel HTTPS with `CONNECT`.

- **Confirm:** `getproxies()` shows `'https': 'https://...'`, or the error disappears with `NO_PROXY=example.com`.
- **Fix:** use `http://` in `HTTPS_PROXY`, in the `proxies=` dict and in `pip --proxy`. On Windows, also check the system proxy settings, which Python reads from the registry.

### 2. TLS client on a plaintext port

Implicit-TLS clients fail immediately when the port expects plain text first.

- **Confirm:** the URL or code uses a port such as 80, 25, 587, 143 or 5432 with a TLS-only client.
- **Fix:** use the matching combination:

```python
import smtplib
# Port 465: implicit TLS
with smtplib.SMTP_SSL("smtp.example.com", 465) as s:
    s.login(user, password)
# Port 587: plain text, then STARTTLS
with smtplib.SMTP("smtp.example.com", 587) as s:
    s.starttls()
    s.login(user, password)
```

The same applies to `imaplib.IMAP4_SSL` (993) versus `IMAP4` plus `starttls()` (143).

### 3. The server does not run TLS on that port

A web server listening on 443 without TLS enabled, or a load balancer forwarding plain HTTP to a port the client treats as HTTPS, produces the same error.

- **Confirm:** `openssl s_client -connect example.com:443 -servername example.com` also fails with `wrong version number`, and `curl http://example.com:443/` returns HTML.
- **Fix:** in Nginx the `listen` directive needs the `ssl` parameter (`listen 443 ssl;`) together with `ssl_certificate` and `ssl_certificate_key`.

### 4. A captive portal or middlebox answering in plain text

Hotel Wi-Fi login pages and some filtering gateways intercept port 443 and reply with an HTTP redirect; see [captive portal SSL errors](https://ssl.sh/errors/captive-portal-ssl-error/).

- **Fix:** complete the portal login, or test from another network to confirm.

## Fixes by environment

Every fix points the TLS client at a port that speaks TLS, and gives the proxy an `http://` URL. curl reports the same mistake as [error 35](https://ssl.sh/errors/curl-35-ssl-connect-error/).

**pip:** `pip install --proxy http://proxy.example.com:8080 package`, or set it once with `pip config set global.proxy http://proxy.example.com:8080`.

**requests:** `requests.get(url, proxies={"https": "http://proxy.example.com:8080"})`. requests also reads environment variables unless `session.trust_env = False`.

**Docker and CI:** proxy variables are often injected by the runner or by the Docker daemon configuration. Print the environment inside the container to see what Python receives.

**Server side:** after fixing the listener, check the result with the [SSL checker](https://ssl.sh/tools/ssl-checker/); for mail servers use the [STARTTLS checker](https://ssl.sh/tools/starttls-checker/).

## Don't do this

- **Switching the URL to `http://` to make the error go away.** The request then travels in clear text, including any tokens or passwords.
- **`verify=False`.** It does not touch this error and removes certificate checks from every later request in the same code path.
- **Forcing an old protocol** such as `ssl.PROTOCOL_TLSv1`. The problem is not the TLS version but the absence of TLS on the other side; downgrading only weakens connections that do work.
