MODULE 1 - HANDS-ON LAB ⏱️ 1-2 hours 💻 6 steps 🎯 Beginner to Intermediate

Build a Multi-API Caller

Apply everything you've learned by building a practical asynchronous tool to fetch data from multiple public APIs

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 requests and aiohttp for 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:

  1. REST Countries API: To get general information about a country (capital, population).
    • URL: https://restcountries.com/v3.1/name/{country_name}
  2. Public Holidays API: To find the next public holiday in that country.
    • URL: https://date.nager.at/api/v3/NextPublicHolidays/{country_code}

Our tool will take a country name as input, fetch data from both APIs concurrently, and print a summary.

STEP 1

Project Setup

⏱️ 5 minutes
Set up your project directory and virtual environment

Instructions

  1. Create a new directory for your lab and navigate into it:
    mkdir api-caller-lab
    cd api-caller-lab
  2. Create and activate a virtual environment using uv:
    uv venv
    source .venv/bin/activate  # On Windows: .venv\Scripts\activate
  3. Create the following files in your directory:
    • main.py (our main script)
    • requirements.txt
    • .gitignore
  4. Add .venv and .env to your .gitignore file.
💡 Tip: If you don't have uv installed, you can use Python's built-in venv:
python -m venv .venv
STEP 2

Install Dependencies

⏱️ 2 minutes

Instructions

  1. Add the following to your requirements.txt:
    aiohttp>=3.9.0
    pydantic>=2.7.1
    python-dotenv>=1.0.1
    requests>=2.31.0
  2. Install the packages:
    uv pip install -r requirements.txt
    Or if using standard pip:
    pip install -r requirements.txt
📥 Download requirements.txt
STEP 3

Define Pydantic Models

⏱️ 10 minutes

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')
💡 Understanding the Models:
  • Field(alias='capitalInfo') maps the API's capitalInfo to Python's capital_info
  • Optional[List[float]] means the field can be None
  • Pydantic will validate the data automatically when we parse API responses
STEP 4

Write Async API Fetchers

⏱️ 20 minutes

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
⚠️ Important: The REST Countries API returns a list of countries. We take the first result using data[0].
STEP 5

Main Orchestration Logic

⏱️ 15 minutes

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("---------------------------------")
💡 Challenge: Can you modify the code to run both API calls concurrently using asyncio.gather()? Hint: You'd need to make an initial call to get the country code first, then use that for the holiday API.
STEP 6

Putting It All Together

⏱️ 5 minutes

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
Expected Output:
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)
---------------------------------
📥 Download Complete Solution (main.py)

🎯 Bonus Challenges

Try these additional challenges to deepen your understanding:

  1. Add User Input: Modify the script to accept a country name from the command line or user input instead of hardcoding it.
  2. Parallel API Calls: Refactor the code to make concurrent API calls using asyncio.gather() when possible.
  3. Add More APIs: Integrate a third API (e.g., weather data) and display that information too.
  4. Error Handling: Add retry logic with exponential backoff for failed API calls.
  5. Save Results: Write the results to a JSON file using the built-in json module.
  6. Add Logging: Replace print() statements with proper logging using Python's logging module.

📥 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 →