Layer 7 Session 42 Real World

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.

GET /countries response.json()

Data from a real, unpredictable place outside your control.

Estimated time: 35โ€“40 minutes  ยท  Pre-quiz โ†’ Concept โ†’ Lab โ†’ Commit โ†’ Post-quiz

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.

Question 1 of 5

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().
Question 2 of 5

What does an HTTP status code of 200 mean, versus 404?

HTTP status codes communicate the outcome of a request. 200 (and the broader 2xx range) means success. 404 means "not found." Checking the status code before calling .json() prevents trying to parse an error page as if it were valid data.
Question 3 of 5

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?

This is exactly Session 30's "contract" concept in action: rather than reshaping our whole application around one specific external API's wide, over-provided shape, we write a small adapter that trims the raw response down to OUR agreed contract, keeping Country and everything downstream of it completely unchanged.
Question 4 of 5

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?

This is the fourth and most significant proof of Session 28's design (after mock data, a JSON file, and now a real live API) โ€” only the source of raw_data changes; every method built on top of the repository remains completely untouched.
Question 5 of 5

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?

An external, real API is the least trustworthy data source of all โ€” you have zero control over it, and it can change or misbehave without warning. This makes Session 30's validation discipline more important here than anywhere else in the project so far.

3. The Concept โ€” Calling a Real API

STATUS 200trust the bodySTATUS 4XX/5XXdo NOT parseas success

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

Lab objective: Call the real REST Countries API, adapt its shape to our contract, and construct a working CountryRepository from genuinely live data.

What you will build

A file called real_api_lab.py.

Step-by-step instructions

1

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")
2

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])
3

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
4

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())
5

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

FileActionWhy
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.
If you find yourself editing any other file, stop. This session touches exactly 2 files.

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"
Do not commit until you can answer out loud: "What did adapt_api_record() have to do, and why did Country and CountryRepository not need any changes at all?"

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.

Question 1 of 5

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().
Question 2 of 5

What does an HTTP status code of 200 mean, versus 404?

HTTP status codes communicate the outcome of a request. 200 (and the broader 2xx range) means success. 404 means "not found." Checking the status code before calling .json() prevents trying to parse an error page as if it were valid data.
Question 3 of 5

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?

This is exactly Session 30's "contract" concept in action: rather than reshaping our whole application around one specific external API's wide, over-provided shape, we write a small adapter that trims the raw response down to OUR agreed contract, keeping Country and everything downstream of it completely unchanged.
Question 4 of 5

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?

This is the fourth and most significant proof of Session 28's design (after mock data, a JSON file, and now a real live API) โ€” only the source of raw_data changes; every method built on top of the repository remains completely untouched.
Question 5 of 5

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?

An external, real API is the least trustworthy data source of all โ€” you have zero control over it, and it can change or misbehave without warning. This makes Session 30's validation discipline more important here than anywhere else in the project so far.

9. Reflection Questions

Think through these after the post-quiz. No right answer โ€” they are for discussion.

  1. 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?
  2. 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?
  3. How does adapt_api_record() relate to Session 30's concept of a "contract" โ€” is the adapter enforcing the contract, or something else?
  4. 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.