Calling a Real API with requests
We finally connect to a live, real network API โ the REST Countries API โ using the requests library, replacing our local file with genuinely external, unpredictable data.
Data from a real, unpredictable place outside your control.
1. Learning Objective
By the end of this session you will be able to:
- Install and use the requests library to make an HTTP GET request
- Read a JSON response body and convert it to Python data structures
- Check an HTTP status code before trusting a response
- Map a real API's response shape onto our existing Country data contract
- Swap CountryRepository's data source to a real API with minimal changes, proving Session 28's design once more
2. Pre-Coding Quiz
Answer these before reading the concept explanation or writing any code. It is fine to get these wrong โ that is the point. You need 4/5 to proceed to the lab.
What does requests.get("https://www.apicountries.com/countries") return?
requests.get() returns a Response object wrapping the HTTP response โ you check .status_code to see if it succeeded, and call .json() to parse the body as JSON into Python data structures, similar to Session 29's json.load().What does an HTTP status code of 200 mean, versus 404?
A real API record looks like {"name": "Kenya", "region": "Africa", "population": 54000000, "area": 580367, "flag": "...", "capital": [...], "currencies": {...}, ...} โ over 20 fields, most of which our Country class does not want. What is needed to use this data with our existing Country class?
Given Session 28's repository design, what actually needs to change to point CountryRepository at data fetched from a real API instead of a JSON file?
Why is directly trusting the API response's data types without validation (skipping Session 30's discipline) risky for a REAL, external API in particular?
3. The Concept โ Calling a Real API
Check the status code before trusting the body โ a failed request's "body" might not even be valid JSON at all.
Making an HTTP request with requests
The requests library (installed via pip) provides a simple interface for making HTTP calls โ a GET request fetches data from a URL.
# pip install requests
import requests
response = requests.get("https://www.apicountries.com/countries")
print(response.status_code) # 200 means success
data = response.json() # parses the JSON body into Python data structures
print(type(data)) # <class 'list'>
print(len(data)) # a few hundred countries
Checking the status code before trusting the response
Just like Session 11's validation-before-use discipline, always check that a request actually succeeded before trying to use its data.
response = requests.get("https://www.apicountries.com/countries")
if response.status_code == 200:
data = response.json()
else:
print(f"API request failed with status {response.status_code}")
data = []
Adapting the API's shape to our own contract
A real, public API almost never returns exactly the shape you want. This particular API returns over 20 fields per country โ area, flags, currencies, calling codes, borders โ when our contract only needs three. An adapter function trims the response down to OUR agreed shape, so Country itself never needs to change or grow to accommodate data it does not use.
def adapt_api_record(raw):
# The real API gives us 20+ fields; our contract only wants 3.
# .get() with a default protects us if a field is ever missing.
return {
"name": raw.get("name", "Unknown"),
"region": raw.get("region", "Unknown"),
"population": raw.get("population", 0),
}
api_records = [
{
"name": "Kenya", "region": "Africa", "population": 54000000,
"area": 580367, "flag": "๐ฐ๐ช", "capital": ["Nairobi"],
"currencies": {"KES": {"name": "Kenyan shilling"}},
},
]
adapted = [adapt_api_record(r) for r in api_records]
print(adapted) # [{'name': 'Kenya', 'region': 'Africa', 'population': 54000000}]
# Now this matches OUR contract exactly โ area, flag, currencies are simply dropped,
# and Country.from_dict() works completely unchanged
Pointing the repository at the real API
Session 28's design proves itself for the third time: only how raw_data is obtained changes.
def fetch_countries_from_api():
response = requests.get("https://www.apicountries.com/countries")
if response.status_code != 200:
return []
return [adapt_api_record(r) for r in response.json()]
repo = CountryRepository(raw_data=fetch_countries_from_api())
print(len(repo.get_all())) # real, live data โ get_all() itself is completely unchanged
4. Lab
What you will build
A file called real_api_lab.py.
Step-by-step instructions
Create the file and make a basic request, checking the status code
# real_api_lab.py
import requests
response = requests.get("https://www.apicountries.com/countries")
print("Status code:", response.status_code)
if response.status_code == 200:
raw_data = response.json()
print("Received", len(raw_data), "records")
else:
raw_data = []
print("Request failed")
Inspect one raw record's actual shape
Print the first record's keys and note how many more fields it has than our own contract.
if raw_data:
print(sorted(raw_data[0].keys()))
print(raw_data[0])
Write adapt_api_record and adapt the whole batch
def adapt_api_record(raw):
return {
"name": raw.get("name", "Unknown"),
"region": raw.get("region", "Unknown"),
"population": raw.get("population", 0),
}
adapted_records = [adapt_api_record(r) for r in raw_data]
print(adapted_records[0]) # now matches our own flat 3-field contract, extras dropped
Build a CountryRepository from the adapted, real data
from country_explorer import CountryRepository
repo = CountryRepository(raw_data=adapted_records)
print(len(repo.get_all()))
print(repo.get_all()[0].summary())
Wrap the whole fetch in a function with status-code handling, and reuse Session 30's validation on a sample record
from country_explorer import validate_country_record
def fetch_and_build_repository():
response = requests.get("https://www.apicountries.com/countries")
if response.status_code != 200:
print(f"API request failed with status {response.status_code}")
return CountryRepository(raw_data=[])
adapted = [adapt_api_record(r) for r in response.json()]
return CountryRepository(raw_data=adapted)
repo = fetch_and_build_repository()
sample = repo._raw_data[0] if repo._raw_data else None
if sample:
validate_country_record(sample) # confirms the real, adapted data honors our contract
print("Sample record honors the contract")
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
real_api_lab.py |
Created | Fetches real data from the REST Countries API, adapts its shape, and builds a working repository. |
docs/sessions/session-42/index.html |
Created | This session document. |
6. Commit Checkpoint
Once the lab is complete and you can explain every line, make this exact commit:
git add real_api_lab.py docs/sessions/session-42/index.html
git commit -m "session-42: fetch real data from the REST Countries API and adapt it to our contract"
7. Code Review Checklist
Go through your code line by line and check each item:
8. Post-Coding Quiz
Same 5 questions. Take it again now that you have written and run the code. You need 4/5 to mark this session complete.
What does requests.get("https://www.apicountries.com/countries") return?
requests.get() returns a Response object wrapping the HTTP response โ you check .status_code to see if it succeeded, and call .json() to parse the body as JSON into Python data structures, similar to Session 29's json.load().What does an HTTP status code of 200 mean, versus 404?
A real API record looks like {"name": "Kenya", "region": "Africa", "population": 54000000, "area": 580367, "flag": "...", "capital": [...], "currencies": {...}, ...} โ over 20 fields, most of which our Country class does not want. What is needed to use this data with our existing Country class?
Given Session 28's repository design, what actually needs to change to point CountryRepository at data fetched from a real API instead of a JSON file?
Why is directly trusting the API response's data types without validation (skipping Session 30's discipline) risky for a REAL, external API in particular?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Why did the real API's much wider raw shape (20+ fields) not require rewriting Country itself? What design decision from earlier sessions made this possible?
- What would happen in your current code if the REST Countries API were temporarily down (status code other than 200)? Is that handled gracefully right now?
- How does adapt_api_record() relate to Session 30's concept of a "contract" โ is the adapter enforcing the contract, or something else?
- This is the third data source CountryRepository has been pointed at (mock, JSON file, real API). What does that consistency tell you about the value of Session 28's original design decision?
10. What Breaks If This Knowledge Is Missing?
- Trusting an untrustworthy response: Skipping the status-code check means a failed request (e.g. a 500 server error, whose body might not even be valid JSON) could crash the program trying to call .json() on something that was never a successful response in the first place.
- Graceful degradation (Session 43): The next session builds proper loading and error states around exactly this kind of network call โ right now, a slow or failed API call has no user-facing feedback at all, which the next session addresses.
- The capstone review (Session 44): This session is the culmination of Layer 4's entire mock-data philosophy: everything built against fake data throughout the project now works, unmodified, against a real, live, external data source.
11. What We Learned
Python concept mastered: Making real HTTP requests with the requests library, checking status codes, and adapting an external API's shape to our own internal data contract.
Unlocks: The Country Explorer can now pull genuinely live, real-world data โ proving every layer of the architecture built since Layer 4 was worth the investment.
Next session: Session 43 โ Handling Errors and Edge Cases Gracefully. We add proper loading and error feedback around this real network call, instead of a program that just silently hangs or fails.