Basic HTTP client use
- Lesson 11
- 45 min
- Module 4: Files, data and external services
Lesson 11 of 18 · Python for Practical AI EngineeringBasic HTTP client use
1. Computer and environment basics
2. Values and control flow
3. Functions and program structure
4. Files, data and external services
5. Debugging and reliability
6. From notebook to maintainable project
What you will be able to do
Get the ticket records from a web service instead of a file, and make the program fail clearly when the service has a problem. Most data in AI work arrives this way, including the answers from AI models.
NotebookDo this lesson in a notebook
The notebook has every step, task and check of this lesson, with the instructions. Its first cells make the ticket-cleaner folder as at the start of this lesson. They also install httpx and download the data files. So the notebook works without your local project. Read the explanations here, and run the code there.
Colab and Kaggle open your own copy. They need a free Google or Kaggle account. Stuck? See the solution notebook.
Before you startWhat to know and to have ready
What you need to know
read_rows,split_recordsandbuild_summary, from Reading and writing files.- JSON in Python, from Reading and writing files: CSV and JSON.
- Packages and dependencies, from Isolate dependencies.
What you need on your computer (only for the local route; the notebook needs none of it)
- The project reads
data/tickets.csvand writesreports/summary.json. You made it in Reading and writing files: Guided practice. If it is missing or different, do that section again. You can also copy the files from the setup cell of this lesson’s notebook. It has every file as at the start of this lesson. data/tickets.csvanddata/tickets.json. You downloaded them in Reading and writing files: Get the data file. If one is missing or changed, download it again with the samecurlcommand. The checksums are in the dataset card.- The project’s environment
.venv, active in your terminal (the prompt starts with(.venv)). You made it in Isolate dependencies: Guided practice. If it is missing or broken, delete it and make it again from the lock file, as in Make the environment again from nothing. - httpx 0.28.1 in that environment. You installed it in Isolate dependencies: Worked example. Check it with
python -c "import httpx; print(httpx.__version__)". If it is missing, runpython -m pip install -r requirements-lock.txt. - An internet connection. The lesson downloads one small public file. There is no account and no cost.
Tested with Python 3.14.6 and httpx 0.28.1 on macOS. The notebook was tested in Jupyter.
Request and response
Section titled “Request and response”When your program gets data from a web service, it is the client, and the service is the server. They talk with HTTP, the same method that your browser uses for web pages. Each exchange has two parts:
| Part | Contains | Example |
|---|---|---|
| Request (client → server) | A method, a URL, and headers. | GET https://…/tickets.json, with the header Accept: application/json |
| Response (server → client) | A status code, headers, and a body. | 200 OK, the header Content-Type: application/json, and the JSON text |
The method says what the client wants. GET means “send me this resource”. Other methods, such as POST, send data to the server. This lesson uses GET only. Headers are extra facts as name–value pairs, for example the type of the body. The body is the data itself, here JSON text.
The status code is a three-digit number that says how the request went. The first digit gives the group:
| Code | Name | Meaning for your program |
|---|---|---|
200 |
OK | Success. The body has the data. |
400 |
Bad Request | Your request is wrong. Retrying the same request does not help. |
401 |
Unauthorized | The service needs a valid key or token. See the next lesson. |
404 |
Not Found | No resource at this URL. Check the URL. |
429 |
Too Many Requests | You sent too many requests. Wait, then try again. |
500 |
Internal Server Error | The server has a problem. Not your fault. |
503 |
Service Unavailable | The server is busy or down. Try again later. |
2xx means success, 4xx means a problem with the request, and 5xx means a problem on the server. Your program must check the code before it uses the body. A 404 response also has a body, but the body is an error page, not your data.
Worked example: get the tickets
Section titled “Worked example: get the tickets”The labs repository publishes the same 15 tickets as JSON at a public URL. Amira tests the request in Python’s interactive mode first. In the terminal, with the environment active, she runs python and types each line after >>>:
>>> import httpx>>> url = "https://raw.githubusercontent.com/Nextia-Intelligence/nextia-learning-labs/main/C02/data/tickets.json">>> response = httpx.get(url, timeout=10.0)>>> response.status_code200>>> response.headers["content-type"]'text/plain; charset=utf-8'>>> rows = response.json()>>> len(rows)15>>> rows[0]{'id': 'T-1001', 'status': 'open', 'category': 'billing', 'priority': 2, 'subject': 'Charged twice for March'}response.json() decodes the JSON body into Python values, as json.load does for a file. The result is the same list of dictionaries that read_rows gives for tickets.json. (This server says text/plain for every file. A real web service says application/json.)
timeout=10.0 means: if the server does not answer within 10 seconds, stop waiting and raise an error. Without a limit, a program can wait for a very long time for a server that never answers.
Then she asks for a file that does not exist:
>>> missing = httpx.get(url.replace("tickets.json", "nothing.json"), timeout=10.0)>>> missing.status_code404>>> missing.text'404: Not Found'>>> missing.raise_for_status()Traceback (most recent call last): ...httpx.HTTPStatusError: Client error '404 Not Found' for url 'https://raw.githubusercontent.com/Nextia-Intelligence/nextia-learning-labs/main/C02/data/nothing.json'raise_for_status() does nothing for a 2xx code, and raises an error for a 4xx or 5xx code. Call it before you read the body. Then a wrong URL or a broken server stops the program with a clear message, instead of becoming wrong data later. To leave interactive mode, she types exit().
The fetch function
Section titled “The fetch function”Amira puts the request into a module, remote.py. Its contract matches read_rows: it returns a list of raw records, so the rest of the program does not care where they came from.
import httpx
def fetch_rows(url: str, client: httpx.Client | None = None) -> list[dict]: """Download raw records as JSON from a ticket service.""" client = client or httpx.Client(timeout=10.0) with client: response = client.get(url) response.raise_for_status() return response.json()| Line | Why |
|---|---|
client: httpx.Client | None = None |
The caller can give a ready-made client. Normal code does not, and gets a new one. Tests give a client with a fake network. See the next section. |
client or httpx.Client(timeout=10.0) |
or gives the left value if it is not None, or else the right one. The new client has a 10-second timeout for every request. |
with client: |
A client keeps network connections open. The with block closes them at the end, as for a file. |
response.raise_for_status() |
Stop with an error for 4xx and 5xx, before the body is used. |
This diagram shows where the new function fits. The rest of the program does not change:
Text description of the diagram
There are three ways to get raw rows (a list[dict]). read_rows reads a CSV or JSON file. fetch_rows downloads JSON from a web service. In a test, a mock response replaces the network for fetch_rows. split_records then checks every row. Rows with no problems become Ticket objects in the tickets list. Rows with problems go to the rejected list with their reasons. build_summary uses both lists, and write_json saves reports/summary.json.
Test without the network: mocked responses
Section titled “Test without the network: mocked responses”How do you check that your program handles a 503 from the server? You cannot ask a real server to fail when you want. And a check that needs the internet fails on a train, and is slow.
A mock is a stand-in for a real part, which you control. httpx has MockTransport: a client that does not use the network, and calls your function to make each response instead. Amira writes a small script to see both cases:
import httpx
from ticket_cleaner.remote import fetch_rows
def answer_ok(request: httpx.Request) -> httpx.Response: return httpx.Response(200, json=[{"id": "T-2001", "status": "open"}])
def answer_unavailable(request: httpx.Request) -> httpx.Response: return httpx.Response(503, text="Service Unavailable")
def main() -> None: client = httpx.Client(transport=httpx.MockTransport(answer_ok)) print(fetch_rows("https://tickets.example/api/tickets", client=client))
client = httpx.Client(transport=httpx.MockTransport(answer_unavailable)) fetch_rows("https://tickets.example/api/tickets", client=client)
if __name__ == "__main__": main()[{'id': 'T-2001', 'status': 'open'}]...httpx.HTTPStatusError: Server error '503 Service Unavailable' for url 'https://tickets.example/api/tickets'No request left the computer: the domain tickets.example does not even exist. Each mock function gets the request, and returns the response that you want to test. The first call gives data. The second stops with a clear error, before any bad data reaches split_records. In Module 5, checks like these become automatic tests.
Guided practice: a second source
Section titled “Guided practice: a second source”-
Make
ticket_cleaner/remote.pywith the code above. -
Make
try_mock.pyin the project folder, and run it:python try_mock.py. You see the list, then the503error. -
In
count_tickets.py, add a choice of source. Add these imports and constants at the top:count_tickets.py (top) from pathlib import Pathfrom ticket_cleaner.files import read_rows, write_jsonfrom ticket_cleaner.parsing import split_recordsfrom ticket_cleaner.remote import fetch_rowsfrom ticket_cleaner.report import build_summarySOURCE = "remote" # a file path, or "remote"TICKETS_URL = "https://raw.githubusercontent.com/Nextia-Intelligence/nextia-learning-labs/main/C02/data/tickets.json"OUTPUT_FILE = Path("reports/summary.json") -
Add a function that loads the rows from the source, and use it in
main()instead ofread_rows(INPUT_FILE):count_tickets.py def load_rows(source: str) -> list[dict]:"""Return raw records from a file, or from the web service for "remote"."""if source == "remote":return fetch_rows(TICKETS_URL)return read_rows(Path(source))def main() -> None:rows = load_rows(SOURCE)# ... the rest of main() does not change -
Run
python count_tickets.py. The output is the same as for the CSV file:15 rows: 9 valid, 6 rejected. -
Change
SOURCEto"data/tickets.csv", and run it again. The result is the same.
Your turn: test a 401 and a 200 with bad data
Section titled “Your turn: test a 401 and a 200 with bad data”Add two mock functions to try_mock.py, and call fetch_rows with each one. Predict the result first.
answer_unauthorized: status401, with the text"missing token".answer_bad_rows: status200, with the JSON list[{"id": "T-2002", "status": "opne"}].
Show the expected result
The 401 raises httpx.HTTPStatusError: Client error '401 Unauthorized' ..., in the same way as the 503. The 200 returns the list without an error: fetch_rows checks only that the request worked. It does not check the records. That is the job of split_records, which rejects the record for missing category, missing priority and unknown status 'opne'. Each function has one job, and the diagram shows where each check happens.
Common mistake
A JSON error from a missing page
Symptom: the program stops in response.json() with an error that does not mention HTTP:
json.decoder.JSONDecodeError: Extra data: line 1 column 4 (char 3)Cause: the server answered with an error, for example 404, and the body is an error page such as 404: Not Found, not JSON. The code read the body without checking the status code first. The message talks about JSON, so it hides the real problem.
Fix:
- Call
response.raise_for_status()beforeresponse.json(). The error now names the real problem:404 Not Foundand the URL. - Check the URL for a typing mistake. Open it in a browser to see what the server sends.
- For a
5xxcode, the problem is on the server. Try again later.
Check your understanding
A request returns status 503. Is the problem in your program or on the server? What should the program do?
Show the answer
On the server: 5xx codes are server problems, and 503 means busy or down. The program must not use the body as data. raise_for_status() stops it with a clear error. A person, or a later version of the program, can try again later. A 4xx code is different: the same request fails again until you change it.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
Why does fetch_rows accept a client argument, if normal code never passes one?
Show the answer
So that a test can pass a client with MockTransport. Then the test controls every response, including errors such as 503, without the network. The normal code path stays simple: with no argument, the function makes a real client with a timeout.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Recap
- A request has a method, a URL and headers. A response has a status code, headers and a body.
2xxis success,4xxis a problem with the request, and5xxis a problem on the server.- Call
raise_for_status()beforeresponse.json(), so an error page never becomes data. - Always set a timeout. httpx clients take
timeout=10.0. - A function that accepts a client can be tested with
MockTransport, without the network.
Next, you move the URL and the choice of source out of the code. Then you add a secret token, and keep it out of every file that others can see.