HTTP, from Python's Side

What a client actually sends, what a server has to answer, and the handful of status codes you will really use.

Syntaxfrom http import HTTPStatus; HTTPStatus.NOT_FOUND == 404 # True

HTTP has a fixed shape, and every Python web library — client or server — is a way of turning that shape into objects. A request is a method, a target (path plus query string), headers, and an optional body. A response is a status code, headers, and an optional body. There is nothing else in it.

The parts that matter

The method states intent: GET reads, POST creates, PUT replaces, PATCH changes part, DELETE removes. GET, PUT and DELETE are idempotent — sending one twice has the same effect as sending it once — and that property is what later decides whether a retry is safe. Headers carry the metadata: Content-Type says what the body is, Accept what you want back, Authorization who you are. The body is always bytes; that there is a JSON object inside is a convention announced by Content-Type: application/json.

Status codes live in an enum

http.HTTPStatus is an IntEnum covering every registered code, each member carrying a phrase and a description. Because it subclasses int, HTTPStatus.NOT_FOUND and the literal 404 are the same value and compare equal — the name is shorthand for the number, and either spelling works anywhere a status is expected. Since Python 3.12 every member also answers is_success, is_client_error and is_server_error, which is better than writing range checks by hand.

The codes you will actually meet: 200, 201 and 204; 400, 401, 403, 404, 409, 422 and 429; 500, 502, 503 and 504.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Deciding whether a failed call is safe to send again.
  • Reading an API's reference and knowing which statuses your client must handle.
  • Choosing the status your own endpoint returns instead of answering 200 for everything.
  • Debugging a request by comparing what you meant to send with what went on the wire.

More examples

The same exchange as bytes on the wire

Worth reading once, because every abstraction above it is shaped like this. The blank line separates headers from body, Content-Length counts bytes and not characters, and 201 carries a Location header pointing at the thing that was created. Reach for this view when a library's behaviour surprises you and you need to see what it really sent.

Example · text
POST /v1/orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
Content-Length: 24

{"sku": "A-1", "qty": 2}

HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/orders/1041
Content-Length: 12

{"id": 1041}

Idempotency, which decides what you may resend

The rule a retry layer needs. When you wire up retries, this predicate is the gate: resend idempotent methods freely, and resend a POST only when the server has agreed to deduplicate on a key you send.

Example · python
# Safe to repeat: the result after two identical calls equals the result
# after one. This is a property of the METHOD, not of your code being careful.
IDEMPOTENT = {"GET", "HEAD", "OPTIONS", "PUT", "DELETE"}

def safe_to_resend(method, has_idempotency_key=False):
    # POST creates something new each time, so a blind retry can double-charge
    # a card or create two orders. An idempotency key the server remembers is
    # what makes a POST retryable.
    return method.upper() in IDEMPOTENT or has_idempotency_key

print(safe_to_resend("PUT"))                          # True
print(safe_to_resend("POST"))                         # False
print(safe_to_resend("POST", has_idempotency_key=True))  # True

Classifying a status without magic numbers

The is_* properties arrived in Python 3.12 and replace the `500 <= code < 600` comparisons you find in older code. Use it at the boundary where you decide between retrying, surfacing an error to the user, and carrying on.

Example · python
from http import HTTPStatus

def interpret(code):
    status = HTTPStatus(code)        # raises ValueError on an unknown code
    if status.is_server_error:       # 5xx: the server broke, try again later
        return "retry"
    if status.is_client_error:       # 4xx: your request was wrong, fix it
        return "fix the request"
    return "ok"

for code in (200, 404, 503):
    print(code, HTTPStatus(code).phrase, "->", interpret(code))

Discussion

  • Be the first to comment on this lesson.