Lab Overview
In this hands-on lab, you'll combine all the concepts from Module 1 to build a practical, asynchronous command-line tool. This tool will fetch data from multiple public APIs concurrently, validate the responses using Pydantic, and display a formatted summary.
💡 What You'll Learn
- Create a Python project using
uv - Use
requestsandaiohttpfor API calls - Manage environment variables with
python-dotenv - Define robust data models using Pydantic
- Handle errors gracefully
- Produce clean, formatted output
Lab Objectives
The Scenario
We want to get information about a specific country. We'll use two free, public APIs:
- REST Countries API: To get general information about a country (capital, population).
- URL:
https://restcountries.com/v3.1/name/{country_name}
- URL:
- Public Holidays API: To find the next public holiday in that country.
- URL:
https://date.nager.at/api/v3/NextPublicHolidays/{country_code}
- URL:
Our tool will take a country name as input, fetch data from both APIs concurrently, and print a summary.
Project Setup
Instructions
- Create a new directory for your lab and navigate into it:
mkdir api-caller-lab cd api-caller-lab - Create and activate a virtual environment using
uv:uv venv source .venv/bin/activate # On Windows: .venv\Scripts\activate - Create the following files in your directory:
main.py(our main script)requirements.txt.gitignore
- Add
.venvand.envto your.gitignorefile.
uv installed, you can use Python's built-in venv:
python -m venv .venv
Install Dependencies
Instructions
- Add the following to your
requirements.txt:aiohttp>=3.9.0 pydantic>=2.7.1 python-dotenv>=1.0.1 requests>=2.31.0 - Install the packages:
Or if using standard pip:uv pip install -r requirements.txtpip install -r requirements.txt
Define Pydantic Models
Instructions
In main.py, define the Pydantic models that represent the data you expect from the APIs. This is your "schema."
# main.py
from pydantic import BaseModel, Field
from typing import List, Optional
# --- Models for REST Countries API ---
class CountryName(BaseModel):
common: str
official: str
class CapitalInfo(BaseModel):
latlng: Optional[List[float]] = None
class CountryInfo(BaseModel):
name: CountryName
cca2: str # The 2-letter country code
capital: List[str]
population: int
capital_info: CapitalInfo = Field(alias='capitalInfo')
# --- Models for Public Holidays API ---
class Holiday(BaseModel):
date: str
local_name: str = Field(alias='localName')
name: str
country_code: str = Field(alias='countryCode')
Field(alias='capitalInfo')maps the API'scapitalInfoto Python'scapital_infoOptional[List[float]]means the field can beNone- Pydantic will validate the data automatically when we parse API responses
Write Async API Fetchers
Instructions
In main.py, write the coroutines to fetch data from each API.
# main.py (continued)
import asyncio
import aiohttp
from pydantic import ValidationError
async def fetch_country_info(session: aiohttp.ClientSession, country_name: str) -> Optional[CountryInfo]:
"""Fetches and validates country information."""
url = f"https://restcountries.com/v3.1/name/{country_name}"
print(f"Fetching data from {url}...")
try:
async with session.get(url) as response:
response.raise_for_status()
data = await response.json()
# The API returns a list, we validate the first result
if data:
return CountryInfo.model_validate(data[0])
return None
except (aiohttp.ClientError, ValidationError, IndexError) as e:
print(f"Error fetching or validating country data: {e}")
return None
async def fetch_next_holiday(session: aiohttp.ClientSession, country_code: str) -> Optional[Holiday]:
"""Fetches and validates the next public holiday."""
url = f"https://date.nager.at/api/v3/NextPublicHolidays/{country_code}"
print(f"Fetching data from {url}...")
try:
async with session.get(url) as response:
response.raise_for_status()
data = await response.json()
if data:
return Holiday.model_validate(data[0])
return None
except (aiohttp.ClientError, ValidationError, IndexError) as e:
print(f"Error fetching or validating holiday data: {e}")
return None
data[0].
Main Orchestration Logic
Instructions
In main.py, write the main coroutine that orchestrates the API calls and displays the results.
# main.py (continued)
async def main(country_name: str):
"""Main function to orchestrate API calls and display results."""
async with aiohttp.ClientSession() as session:
# First, get country info to find the country code
country_data = await fetch_country_info(session, country_name)
if not country_data:
print(f"\nCould not retrieve information for '{country_name}'. Exiting.")
return
# Now, use the country code to get the next holiday
holiday_data = await fetch_next_holiday(session, country_data.cca2)
# Display formatted output
print("\n---------------------------------")
print(f"Information for: {country_data.name.official}")
print("---------------------------------")
print(f"Capital: {', '.join(country_data.capital)}")
print(f"Population: {country_data.population:,}")
print(f"Country Code: {country_data.cca2}")
if holiday_data:
print("\n--- Next Public Holiday ---")
print(f"Date: {holiday_data.date}")
print(f"Name: {holiday_data.name} ({holiday_data.local_name})")
else:
print("\nCould not find holiday information.")
print("---------------------------------")
asyncio.gather()?
Hint: You'd need to make an initial call to get the country code first, then use that for the holiday API.
Putting It All Together
Instructions
Create the entry point for your script at the bottom of main.py:
# main.py (continued)
if __name__ == "__main__":
country = "Canada" # Hardcode for testing, or use input()
print(f"Searching for information about {country}...")
asyncio.run(main(country))
Run Your Application
python main.py
Searching for information about Canada... Fetching data from https://restcountries.com/v3.1/name/Canada... Fetching data from https://date.nager.at/api/v3/NextPublicHolidays/CA... --------------------------------- Information for: Canada --------------------------------- Capital: Ottawa Population: 38,005,238 Country Code: CA --- Next Public Holiday --- Date: 2024-12-25 Name: Christmas Day (Christmas Day) ---------------------------------
🎯 Bonus Challenges
Try these additional challenges to deepen your understanding:
- Add User Input: Modify the script to accept a country name from the command line or user input instead of hardcoding it.
- Parallel API Calls: Refactor the code to make concurrent API calls using
asyncio.gather()when possible. - Add More APIs: Integrate a third API (e.g., weather data) and display that information too.
- Error Handling: Add retry logic with exponential backoff for failed API calls.
- Save Results: Write the results to a JSON file using the built-in
jsonmodule. - Add Logging: Replace
print()statements with proper logging using Python'sloggingmodule.
📥 Download Lab Files
Download all the files you need to complete this lab:
🎉 Lab Complete!
Congratulations! You've successfully built a multi-API caller using Python's async capabilities, Pydantic for data validation, and modern package management with uv.
You're ready to move on to the Module 1 Quiz to test your knowledge!
Take the Quiz →