Chapter 21 Quiz: Working with APIs and External Data Services
Answer all questions before checking the answer key at the end. Questions vary in format: multiple choice, true/false, short answer, and code analysis.
Section A: Multiple Choice (1 point each)
Question 1. What does API stand for?
a) Automated Protocol Interface b) Application Programming Interface c) Advanced Python Integration d) Application Process Input
Question 2. When you call response.json() on a requests response object, what does it return?
a) A JSON string that you must manually parse b) The raw HTTP response body as bytes c) A Python dictionary or list, depending on the response structure d) A file-like object you must iterate over
Question 3. Which HTTP method is used to retrieve data from an API without modifying anything?
a) POST b) PUT c) GET d) FETCH
Question 4. You receive a 401 status code from an API. What does this almost certainly mean?
a) The server is down and you should retry later b) Your request was successful c) The resource you requested does not exist d) Your API key is invalid or missing
Question 5. Which of the following is the correct way to store and access an API key in Python?
a) api_key = "sk_live_abc123" at the top of your script
b) api_key = os.environ.get("MY_API_KEY") after loading from the environment
c) api_key = input("Enter your API key: ") each time the script runs
d) api_key = open("api_key.txt").read() from a plaintext file in the project directory
Question 6. In the requests library, how do you pass query parameters to a GET request?
a) requests.get(url + "?param=value")
b) requests.get(url, data={"param": "value"})
c) requests.get(url, params={"param": "value"})
d) requests.get(url, query={"param": "value"})
Question 7. What does a 429 status code indicate?
a) The server encountered an internal error b) Your request was malformed c) You have been rate limited and sent too many requests d) The endpoint you requested does not exist
Question 8. What is exponential backoff?
a) Increasing the size of each API response to reduce the number of requests needed b) Waiting progressively longer between retry attempts (e.g., 1s, 2s, 4s, 8s...) c) Reducing the number of query parameters with each retry d) Splitting large API responses into exponentially smaller chunks
Question 9. What is the purpose of pagination in an API?
a) To encrypt the response data for security b) To split large result sets into multiple smaller requests rather than returning everything at once c) To compress the response body for faster transmission d) To allow multiple users to share a single API key
Question 10. Which code snippet correctly reads a Bearer token from an environment variable and uses it in an API call?
Option A:
headers = {"Authorization": "Bearer " + "MY_TOKEN"}
Option B:
token = os.environ.get("API_TOKEN")
headers = {"Authorization": f"Bearer {token}"}
Option C:
headers = {"Bearer": os.environ.get("API_TOKEN")}
Option D:
headers = {"Token": "Bearer", "Value": os.environ.get("API_TOKEN")}
a) Option A b) Option B c) Option C d) Option D
Section B: True or False (1 point each)
Question 11. A REST API response always includes a status code, regardless of whether the request succeeded.
Question 12. Calling response.raise_for_status() on a 200 response will raise an exception.
Question 13. Query parameters in a GET request are visible in the URL and therefore should never be used to transmit API keys.
Question 14. The requests library comes pre-installed with Python and does not need to be installed separately.
Question 15. When an API returns a 503 status code (Service Unavailable), retrying the same request later is a reasonable response.
Section C: Code Analysis (2 points each)
Question 16. What is wrong with this code?
import requests
response = requests.get("https://open.er-api.com/v6/latest/USD")
data = response.json()
eur_rate = data["rates"]["EUR"]
print(f"EUR rate: {eur_rate}")
Identify at least two problems and explain how to fix them.
Question 17. Analyze this pagination loop. Is it correct? If not, what could go wrong?
all_results = []
page = 1
while True:
response = requests.get(url, params={"page": page, "pageSize": 50})
data = response.json()
results = data["items"]
all_results.extend(results)
page += 1
Question 18. This retry function has a bug. Find it:
def fetch_with_retry(url, max_retries=3):
for attempt in range(max_retries):
response = requests.get(url, timeout=10)
if response.status_code == 200:
return response.json()
if response.status_code == 429:
time.sleep(2 ** attempt)
raise RuntimeError("All retries failed")
Question 19. What does this code do, and is the approach correct?
def safe_get_revenue(data):
try:
return data["company"]["financials"]["annual"]["revenue"]
except KeyError:
return None
except TypeError:
return None
Section D: Short Answer (3 points each)
Question 20. Priya needs to build a script that makes 500 API calls to fetch data on Acme's 500 customer accounts. The API has a rate limit of 60 requests per minute. Describe how you would structure the loop to stay within the rate limit, including: the calculation for how long to wait between requests, and how to handle a 429 response if you accidentally exceed it.
Question 21. Explain the difference between storing an API key in a .env file versus storing it in a system environment variable. When would you prefer each approach? What must you remember to do with the .env file to ensure the key stays secure?
Question 22. The Open-Meteo weather API returns this JSON structure:
{
"current_weather": {
"temperature": 12.4,
"windspeed": 18.2,
"weathercode": 3,
"time": "2024-10-28T14:00"
},
"hourly": {
"time": ["2024-10-28T00:00", "2024-10-28T01:00"],
"temperature_2m": [10.1, 9.8]
}
}
Write a Python function extract_weather_summary(response_data) that returns a plain English string describing the current conditions — for example: "Currently 12.4°C (54.3°F) with wind at 18.2 km/h (11.3 mph)". Include the Fahrenheit and mph conversions in your implementation.
Question 23. A colleague suggests handling all API errors like this:
try:
response = requests.get(url, timeout=30)
data = response.json()
return data
except Exception as e:
print(f"Something went wrong: {e}")
return None
Explain why this is a poor approach. What are the specific problems? Write a better version.
Answer Key
Section A: 1. b 2. c 3. c 4. d 5. b 6. c 7. c 8. b 9. b 10. b
Section B:
11. True — every HTTP response includes a status code; it is part of the HTTP protocol
12. False — raise_for_status() only raises an exception for 4xx and 5xx status codes; 200 is a success code and raises nothing
13. True — query parameters appear in the URL, in browser history, in server logs, and in any proxy logs between the client and server; headers are a better choice
14. False — requests is a third-party library and must be installed with pip install requests
15. True — 503 Service Unavailable usually indicates a temporary server problem; retrying later (with appropriate backoff) is the correct response
Section C:
-
Problems: (1) No
timeoutparameter — the request could hang indefinitely if the server is slow or unresponsive (2) No status code check before callingresponse.json()— if the response is a 429 or 500,response.json()may fail or return an error structure that doesn't contain "rates" (3)data["rates"]["EUR"]uses direct access rather than.get()— will crash withKeyErrorif the structure is unexpected Fixed version: Addtimeout=15, checkresponse.raise_for_status(), usedata.get("rates", {}).get("EUR") -
Bug: The loop has no termination condition other than receiving an empty results list — but the code never checks for that. If the API always returns at least one item, this loop runs forever. Additionally, there is no
max_pagessafety limit, no delay between requests (likely to trigger rate limiting), and no status code check. Fix: Addif not results: break, add amax_pagesguard, addtime.sleep()between requests, and checkresponse.raise_for_status(). -
Bug:
range(max_retries)iteratesmax_retriestimes (0, 1, 2 whenmax_retries=3) — but the function description says it should trymax_retriestimes before failing. Combined with a non-200, non-429 status code (like 500), the function never retries those errors — it just increments the loop and on the third iteration, exits the loop and raises RuntimeError even though retrying might have worked. Fix: Userange(max_retries + 1)and explicitly handle 500-level errors with retry logic. -
What it does: It attempts to navigate a deeply nested dictionary and returns
Noneif any key is missing (KeyError) or if a value along the path is not a dictionary (TypeError, which would be raised when you try to subscriptNone["key"]). Is it correct? Yes, the approach is valid. Catching bothKeyErrorandTypeErrorhandles the two most common failure modes for nested dict access. A logging call beforereturn Nonewould make debugging easier in production.
Section D:
-
Rate limit compliance: At 60 requests/minute, you can make 1 request per second. Between each request, add
time.sleep(1.0)(or slightly more, liketime.sleep(1.1)) to give a small buffer. For 500 requests, total runtime is approximately 8-9 minutes. For 429 handling: catch the 429 response, read theRetry-Afterheader if present, sleep for that duration (or 60 seconds as a fallback), then continue from where you left off. Use a loop variable to track progress so you can resume without repeating requests if the script is interrupted. -
.env file: Stores credentials in a file in the project directory. Requires
python-dotenvto load. Easy to set up per-project. Can be version controlled by accident if you forget.gitignore. Best for development and personal projects. System environment variable: Set at the OS level, persists across reboots, accessible to all processes for that user. More secure for production servers. Harder to manage per-project. Best for production deployments and shared servers. Critical: The.envfile must be added to.gitignoreto prevent it from being committed to version control. -
Sample answer:
def extract_weather_summary(response_data):
current = response_data.get("current_weather", {})
temp_c = current.get("temperature", 0)
wind_kmh = current.get("windspeed", 0)
temp_f = temp_c * 9 / 5 + 32
wind_mph = wind_kmh * 0.621371
return (
f"Currently {temp_c:.1f}°C ({temp_f:.1f}°F) "
f"with wind at {wind_kmh:.1f} km/h ({wind_mph:.1f} mph)"
)
- Problems: (1) Catches all
Exceptiontypes indiscriminately — you cannot distinguish between a connection problem (no internet), an authentication failure (bad API key), a rate limit (should retry), and a programming error (bug in your code). Each of these requires a different response. (2) ReturnsNonesilently on error — callers have no way to know whether they got real data or a failure. (3) The error message"Something went wrong: {e}"is unhelpful to a user who needs to know what to do. Better version: Catchrequests.exceptions.ConnectionError,requests.exceptions.Timeout,requests.exceptions.HTTPErrorseparately; checkresponse.status_codeexplicitly; raise meaningful exceptions with actionable messages; reserve bareexcept Exceptiononly as a last resort with full logging.
Total points: 10 (Section A) + 5 (Section B) + 8 (Section C) + 12 (Section D) = 35 points
Suggested passing score: 28/35 (80%)