psf/requests · error · ValueError

Invalid timeout . Pass a (connect, read) timeout tuple, or…

Error message

Invalid timeout {timeout}. Pass a (connect, read) timeout tuple, or a single float to set both timeouts to the same value.

What it means

Raised when the timeout argument is a tuple that does not unpack into exactly two values (connect, read). The code calls connect, read = timeout inside a try; a tuple of length != 2 raises ValueError, which is caught and re-raised with this descriptive message. Non-tuple timeouts (float, int, TimeoutSauce) bypass this branch entirely.

Solutions

  1. Pass a single float for equal connect/read timeouts: timeout=5.0.
  2. Pass exactly a 2-tuple: timeout=(connect, read).
  3. Validate dynamic timeouts before sending: assert len(t) == 2 when t is a tuple/list.
  4. Normalize config-driven values through a helper that always returns a float or a 2-tuple.
  5. Add a unit test that exercises the timeout builder with edge inputs.

Example fix

# before
requests.get(url, timeout=(5,))  # -> ValueError: Invalid timeout

# after
requests.get(url, timeout=(5, 30))  # (connect, read)
Defensive patterns

Strategy: type-guard

Validate before calling

def normalize_timeout(t):
    """Always return a float or a 2-tuple (connect, read)."""
    if isinstance(t, (int, float)):
        return float(t)
    if isinstance(t, tuple):
        if len(t) != 2:
            raise ValueError(
                f"timeout tuple must be (connect, read), got {t!r}"
            )
        return (float(t[0]), float(t[1]))
    # assume it's already a urllib3 Timeout
    return t

requests.get(url, timeout=normalize_timeout(config["timeout"]))

Type guard

def is_valid_timeout(t) -> bool:
    if isinstance(t, (int, float)):
        return t >= 0
    if isinstance(t, tuple):
        return (
            len(t) == 2
            and all(isinstance(x, (int, float)) and x >= 0 for x in t)
        )
    from urllib3.util import Timeout
    return isinstance(t, Timeout)

Try / catch

try:
    resp = session.get(url, timeout=timeout)
except ValueError as e:
    if "Invalid timeout" in str(e):
        # fall back to a safe default and retry once
        resp = session.get(url, timeout=30.0)
    else:
        raise

Prevention

When it happens

Trigger: Triggered by passing timeout=(5,) (one element), timeout=(1, 2, 3) (three elements), or any tuple/list whose length is not 2, to request/get/post/send or a Session. The error occurs before any network call, during timeout resolution in send.

Common situations: Happens when callers build timeouts dynamically and accidentally produce a 1- or 3-tuple, when copying a timeout from config that was meant to be a single float but got wrapped, or when using a list instead of a 2-tuple. Easy to introduce via f-string/config-driven timeout assembly.

Understand the failure class

Related errors


AI-assisted analysis of psf/requests@8068356288 (2026-08-11). Data as JSON: /api/errors/64ab0e48615e4134. Report an issue: GitHub.

Appendix: source

Thrown at src/requests/adapters.py:686

        self.cert_verify(conn, request.url, verify, cert)
        url = self.request_url(request, proxies)
        self.add_headers(
            request,
            stream=stream,
            timeout=timeout,
            verify=verify,
            cert=cert,
            proxies=proxies,
        )

        chunked = not (request.body is None or "Content-Length" in request.headers)

        if isinstance(timeout, tuple):
            try:
                connect, read = timeout
                resolved_timeout = TimeoutSauce(connect=connect, read=read)
            except ValueError:
                raise ValueError(
                    f"Invalid timeout {timeout}. Pass a (connect, read) timeout tuple, "
                    f"or a single float to set both timeouts to the same value."
                )
        elif isinstance(timeout, TimeoutSauce):
            resolved_timeout = timeout
        else:
            resolved_timeout = TimeoutSauce(connect=timeout, read=timeout)

        try:
            resp = conn.urlopen(
                method=request.method,
                url=url,
                body=request.body,  # type: ignore[arg-type]  # urllib3 stubs don't accept Iterable[bytes | str]
                headers=request.headers,  # type: ignore[arg-type]  # urllib3#3072
                redirect=False,
                assert_same_host=False,
                preload_content=False,
                decode_content=False,

View on GitHub (pinned to 8068356288)