0%

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 , and the quotes keep it together as one . It saves writing a file for a one-line check.

One request, spelled out in full

Open api_catalog.py. Here is the you are building:

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 after it keyword-only. A caller has to write timeout=(1.0, 1.0) rather than passing a bare in third position. With four similar-looking arguments, being forced to name them is worth the extra typing.

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() , and the function will use that instead. In a test, you hand it a small fake that records what it was asked for and returns whatever you decide. No network is involved. Nothing waits. The result is the same every single time.

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.