Chapter 11 · practice
Talking to an API
Make One Controlled Request
Every input your catalog has read so far came from a file sitting next to it. File operations can fail too, as Chapter 9 showed. A network service adds another boundary: your request and its reply travel between programs, and either side can stop responding.
This chapter makes those request and response boundaries explicit, so your code can distinguish usable data from a failed exchange.
The service here is a practice API that lives inside Python Land, on an isolated network your code can reach but the public internet cannot. It is deliberately boring and deliberately predictable, including the ways it fails. That is what makes it useful to learn on.
Set up the environment and the recipe
Same split as always: a disposable environment, and a durable file that can rebuild it.
python -m venv .venv
.venv/bin/python -m pip install requests==2.34.2
nano requirements.txt
.venv/bin/python -c "import requests; print(requests.__version__)"
Put exactly requests==2.34.2 in requirements.txt, with a newline at the end.
That last command is a small trick worth knowing: python -c runs a short program given directly on the
One request, spelled out in full
Open api_catalog.py. Here is the
def request_json(path, *, params=None, timeout=DEFAULT_TIMEOUT, session=None):
client = requests if session is None else session
response = client.get(
API_BASE + path,
params=params,
headers=PRACTICE_HEADERS,
timeout=timeout,
)
return response.json()
Four things are happening, and each is a decision.
The bare * makes every timeout=(1.0, 1.0) rather than passing a bare
The header goes on every request. X-Practice-Name: python-in-practice identifies this course to the service. It is not a password, and there is nothing secret in this project.
The timeout is not optional. It has a default, but the default is explicit and it is always sent. A request with no timeout can wait indefinitely. Lesson 4 is entirely about this boundary.
The session parameter is the interesting one, so it gets its own section.
Why the function lets you hand it a client
Look at the first line again:
client = requests if session is None else session
Normally, session is None and the function uses requests directly. That is what happens when the program really runs.
But a caller can pass in something else that has a .get()
This is worth pausing on, because it is a genuinely important idea and it is easy to mistake for a trick. You are not building a second way to make requests. You are leaving a seam at the one point where your code touches the outside world, so that the outside world can be stood in for when you need to examine what your code did. Every test you write in this chapter and the capstone depends on that seam existing.
A stand-in like that does not need to be elaborate. It needs a .get() that records what it was handed and returns something with a .json(). types.SimpleNamespace from the standard library is enough to assemble one:
import types
def fake_session(document, calls):
def decode_json():
return document
def get(url, **kwargs):
calls.append((url, kwargs))
return types.SimpleNamespace(json=decode_json)
return types.SimpleNamespace(get=get)
SimpleNamespace builds an object from whatever keyword arguments you give it, so types.SimpleNamespace(get=get) produces something with a .get attribute and nothing else. That is all request_json ever asks for.
Try it:
calls = []
result = request_json("/items", session=fake_session({"items": []}, calls))
print(calls[0])
calls[0] shows you the exact URL, parameters, headers, and timeout your function chose, without a single byte crossing a network.
Try the real one too
The internal service is there if you want to see a live answer:
.venv/bin/python -c "from api_catalog import request_json; print(request_json('/items'))"
Right now this decodes the response body no matter what the server said. If the service answers with an error, this function will cheerfully hand you the error as though it were data. That is a real gap, and Lesson 3 closes it.
Why does request_json accept an optional session?
If the fake session idea has not clicked yet, ask Monty to walk through what client refers to on a normal call and on a test call. It is the one idea in this chapter worth slowing down for.
Task
Record exactly requests==2.34.2 in requirements.txt. Implement request_json as one GET to the fixed internal base, using the injected session when supplied and always passing params, the fixed practice header, and an explicit timeout. Return its decoded JSON for now.