Chapter 8: Weather Dashboard App

Building Professional Multi-API Applications

1. Introduction

Welcome to Chapter 8, where you will build a Weather Dashboard that integrates multiple external APIs into a unified, professional application. Most real software doesn't work with just one data source. Weather apps need geocoding services to convert city names into coordinates, then weather APIs to fetch forecasts for those locations. E-commerce platforms combine payment processors, inventory systems, and shipping APIs. Social media dashboards pull from multiple platforms simultaneously. The ability to coordinate multiple APIs into cohesive applications is fundamental to modern software development.

The challenge isn't making individual API calls. You learned that in Chapter 6. The challenge is orchestration: managing data flow between services, handling cascading failures when one API succeeds but another fails, transforming responses from different formats into consistent structures, and presenting unified results to users. Your weather dashboard will demonstrate these integration patterns by coordinating two distinct APIs with different responsibilities, response formats, and failure modes.

You'll build this systematically. First, you'll understand the coordination problem: why weather applications need multiple APIs and how data flows between them. Then you'll implement each API layer independently, establishing patterns for request construction, error handling, and data extraction. With both layers working reliably, you'll build the integration logic that coordinates them. Finally, you'll add the presentation layer that transforms technical API responses into user-friendly weather displays. Each step builds on previous patterns while adding new complexity.

By the end of this chapter, you'll have created a complete command-line weather dashboard that searches any city globally and displays current conditions plus multi-day forecasts. More importantly, you'll understand how to architect any multi-API integration: identifying service boundaries, managing dependencies between calls, handling partial failures gracefully, and building reliable applications from unreliable external services.

Learning Objectives

By the end of this chapter, you'll be able to:

  • Navigate API documentation systematically: extract essential information from complex documentation and build working requests.
  • Coordinate multiple APIs: chain geocoding and weather services together, managing data flow between different endpoints.
  • Handle integration failures: identify failure points in multi-API workflows and implement appropriate error responses.
  • Process complex JSON responses: extract, transform, and present nested data structures from real-world APIs.
  • Build professional applications: organize code with classes, methods, and separation of concerns for maintainability.
  • Create interactive interfaces: develop command-line applications with user input validation and continuous operation.

Prerequisites

This chapter assumes you're comfortable with:

  • Basic Python programming and the requests library
  • JSON parsing and dictionary manipulation
  • Command-line program execution
  • Basic error handling with try/except blocks

If you need to review any of these concepts, refer to the earlier chapters before proceeding.

This chapter represents a significant step in your development journey. You'll move beyond isolated API calls to building complete applications that integrate multiple systems, handle real-world complexity, and provide genuine utility to users. The weather dashboard serves as both a learning exercise and a template for understanding how professional software combines different services into cohesive solutions.

2. The Coordination Challenge

Weather applications face a fundamental mismatch between how users think and how APIs work. Users think in place names: "What's the weather in Dublin?" or "Show me Tokyo's forecast." Weather APIs think in coordinates: they require precise latitude and longitude values to return forecast data. This gap creates the core integration challenge: you need one service to translate user-friendly input into API-ready coordinates, then a second service to fetch actual weather data for those coordinates.

This two-API pattern appears throughout professional software. Authentication services validate credentials, then user profile APIs fetch account details. Payment processors authorize transactions, then order APIs create purchase records. Search APIs find product IDs, then inventory APIs check availability. The coordination logic (managing dependencies, handling failures, transforming data between services) follows consistent patterns regardless of domain.

Understanding the Data Flow

Your weather dashboard orchestrates two Open-Meteo APIs with distinct responsibilities. The geocoding API converts location names into coordinates. The weather forecast API returns current conditions and multi-day predictions for specific coordinates. Neither can do the other's job, and weather data requires coordinates that only geocoding provides.

User Input

City name (e.g., "Dublin")

API 1: Geocoding API

Converts city name to coordinates

Your Code: Extracts Coordinates

Latitude: 53.3331, Longitude: -6.2489

API 2: Weather API

Fetches weather data for coordinates

Your Code: Processes the Data

Transforms raw JSON into user-friendly output

Weather Display

Current conditions + 5-day forecast

This flow reveals both the external dependencies (the two APIs you don't control) and your application's responsibilities (coordinate extraction and data processing). Each component has different failure modes: geocoding might not find the city, weather API might be temporarily unavailable, responses might contain unexpected data. Professional integration handles these scenarios systematically rather than hoping everything works.

Identifying Failure Points

Multi-API workflows create multiple points where things can go wrong. Understanding these failure modes before building helps you design appropriate responses for each scenario.

1.

User Input Failures

Empty input, typos, nonsense text, cities that don't exist. These require clear user feedback and retry opportunities.

2.

Geocoding Failures

Network timeouts, API rate limits, no results found, malformed responses. Some are temporary (retry), others permanent (inform user).

3.

Weather API Failures

Service unavailable, invalid coordinates, incomplete data. Even when geocoding succeeds, weather data might fail.

4.

Data Processing Failures

Missing fields, unexpected types, values outside realistic ranges. Raw API responses need validation before use.

The Cascading Failure Problem

In multi-API workflows, failure at any step prevents subsequent steps from running. If geocoding fails, you never reach the weather API. If weather data is malformed, you can't display results. This creates a chain where the final output depends on every component succeeding. Professional applications identify which failures are recoverable (retry network issues) versus terminal (city doesn't exist) and respond appropriately to each.

Designing Clean Interfaces

Each layer in your application should present a clean interface to the next layer. The geocoding layer returns coordinates or signals failure. The weather layer returns formatted data or signals failure. The display layer shows results or explains what went wrong. This separation makes each component independently testable and the overall system easier to understand.

Interface Design Pattern
Python - Clean Layer Interfaces
# Geocoding layer interface
def find_location(city_name):
    """
    Returns: (latitude, longitude, formatted_name) on success
             (None, None, None) on failure
    """
    # Implementation handles all geocoding complexity
    # Calling code only checks if latitude is None
    
# Weather layer interface  
def get_weather_data(latitude, longitude):
    """
    Returns: weather_dict on success
             None on failure
    """
    # Implementation handles all weather API complexity
    # Calling code only checks if result is None

# Display layer interface
def display_weather(weather_data, location_name):
    """
    Displays formatted weather information.
    Assumes weather_data has been validated.
    """
    # No return value - just formats and prints

These simple interfaces hide internal complexity while providing clear success/failure signals. Geocoding either produces coordinates or doesn't. Weather data either exists or doesn't. The integration logic coordinates these layers without needing to understand their internal implementation details.

Building Strategy

You'll implement this weather dashboard in systematic stages that mirror professional development practices: understand requirements, build components independently, integrate components, add user interface polish.

1.

Documentation Analysis

Extract requirements from Open-Meteo API documentation: required parameters, response formats, error conditions.

2.

Geocoding Implementation

Build the location lookup layer with error handling and clean return interface.

3.

Weather Implementation

Build the weather data layer, applying the same patterns established for geocoding.

4.

Integration Layer

Coordinate both APIs: call geocoding, validate results, call weather API, handle cascading failures.

5.

Application Shell

Add interactive interface, display formatting, and user experience polish.

This progression ensures each component works reliably before combining them. Testing geocoding independently is simpler than debugging the complete integrated system. Once both APIs work individually, the integration logic becomes straightforward.

Takeaways & Next Step

Multi-API Coordination Fundamentals:

  • Sequential dependencies: Weather data requires coordinates from geocoding (one API's output becomes another's input)
  • Multiple failure modes: Each layer can fail independently, requiring different response strategies
  • Clean interfaces: Each layer presents simple success/failure signals hiding internal complexity
  • Systematic building: Implement and test components independently before integration

With the coordination architecture understood, Section 3 shows how to extract the information you need from API documentation systematically, transforming reference material into working request specifications.

3. Reading API Documentation Systematically

Reading documentation is often the most tedious part of integration, but skipping it is the fastest way to write broken code. Before writing any code, professional developers extract four critical pieces of information: what endpoints to call, what parameters to send, what responses to expect, and what errors might occur. This systematic analysis prevents the trial-and-error approach that leads to fragile integrations and wasted debugging time.

Open-Meteo provides clear, well-structured documentation that demonstrates how professional API providers organize information. You'll use their geocoding and weather forecast APIs, learning patterns that apply to any API you encounter. The goal isn't memorizing Open-Meteo specifics. It's developing a repeatable process for understanding unfamiliar APIs quickly.

Locating Documentation

Quality API providers make documentation easily discoverable with clear navigation and comprehensive coverage of all endpoints. Open-Meteo follows this pattern with separate documentation pages for each API service.

1.

Main Weather API Documentation

Navigate to https://open-meteo.com/en/docs

This page documents the weather forecast API that provides current conditions and multi-day predictions.

2.

Geocoding API Documentation

Navigate to https://open-meteo.com/en/docs/geocoding-api

This page documents the geocoding API that converts location names to coordinates.

Identify API Documentation

Professional API documentation follows predictable structures, though terminology can vary. Look for these sections:

  • Endpoint / API URL: The address you send requests to.
    Watch out: Many docs (like Open-Meteo) show this as a full example link (e.g., .../v1/forecast?latitude=52...). The actual endpoint is just the part before the question mark (?).
  • Parameter tables: Lists of required versus optional settings (like `latitude`, `days`, `units`).
  • Response examples: Sample JSON showing the structure of the data you will receive.
  • Error documentation: A list of HTTP status codes (like 404, 400) and what they mean for this specific API.
  • Rate limits: Rules about how many requests you can make per minute or hour.

With the documentation open, we can now shift from browsing to analysis. In the following two subsections, we will systematically extract the specific endpoints and parameters needed for our dashboard, starting with the Geocoding API.

Extracting Geocoding Requirements

Start with the geocoding API since weather data depends on its output. Your goal is answering four questions: What's the endpoint? What parameters are required? What optional parameters improve results? What does the response look like?

Geocoding API Analysis
1.

Base Endpoint

Endpoint URL
https://geocoding-api.open-meteo.com/v1/search

All geocoding requests go to this URL with query parameters appended.

2.

Required Parameters

The documentation identifies one required parameter:

  • name - The location name to search for (minimum 2 characters)
3.

Useful Optional Parameters

Several optional parameters improve result quality:

  • count - Number of results to return (default: 10, maximum: 100)
  • language - Response language (default: English)
  • format - Response format, use "json" explicitly
4.

Response Structure

The documentation shows responses contain a results array with location objects. Each location includes:

  • name - City or location name
  • latitude - Geographic latitude
  • longitude - Geographic longitude
  • country - Country name
  • admin1 - State or region (when available)
What This Analysis Provides

You now know exactly what to send (endpoint + parameters) and what to expect back (results array structure). This information becomes your implementation specification. You don't need to understand every field in the documentation. Focus on the minimum needed for your use case.

Extracting Weather API Requirements

The weather forecast API offers extensive parameters for different weather data. Your task is identifying which parameters provide the information needed for a useful weather dashboard: current conditions and multi-day forecasts.

Weather API Analysis
1.

Base Endpoint

Endpoint URL
https://api.open-meteo.com/v1/forecast
2.

Required Parameters

  • latitude - Geographic latitude (from geocoding API)
  • longitude - Geographic longitude (from geocoding API)
3.

Current Weather Parameters

The documentation organizes weather data into categories. For current conditions, select from the current parameter group:

  • temperature_2m - Air temperature at 2 meters
  • relative_humidity_2m - Humidity percentage
  • apparent_temperature - "Feels like" temperature
  • weather_code - Standardized weather condition code
  • wind_speed_10m - Wind speed at 10 meters
  • wind_direction_10m - Wind direction in degrees
4.

Daily Forecast Parameters

For multi-day forecasts, select from the daily parameter group:

  • temperature_2m_max - Daily maximum temperature
  • temperature_2m_min - Daily minimum temperature
  • weather_code - Daily weather condition
  • precipitation_sum - Total daily precipitation
  • wind_speed_10m_max - Maximum daily wind speed
5.

Configuration Parameters

  • timezone - Use "auto" for location-appropriate timestamps
  • forecast_days - Number of days to forecast (1-16)
Parameter Selection Strategy

The weather API offers dozens of parameters. Rather than requesting everything, identify the minimum set that provides useful information. For a weather dashboard: current temperature and conditions for immediate conditions, daily min/max temperatures for planning. This focused approach keeps responses manageable and processing simple.

Understanding Response Formats

Documentation typically includes example responses showing the actual JSON structure you'll receive. These examples are critical for writing data extraction code.

Geocoding Response Structure
JSON - Geocoding Response Example
{
  "results": [
    {
      "id": 2964574,
      "name": "Dublin",
      "latitude": 53.33306,
      "longitude": -6.24889,
      "elevation": 17.0,
      "timezone": "Europe/Dublin",
      "country": "Ireland",
      "country_id": 372,
      "admin1": "Leinster",
      "admin1_id": 7521314
    }
  ],
  "generationtime_ms": 0.48708916
}

Key observations: Results come in an array even for single matches. Each result is a dictionary with many fields. You need latitude, longitude, and name for your use case. Other fields can be ignored.

Weather Response Structure
JSON - Weather Response Example (simplified)
{
  "latitude": 53.33,
  "longitude": -6.25,
  "timezone": "Europe/Dublin",
  "current": {
    "time": "2024-01-15T14:00",
    "temperature_2m": 8.3,
    "relative_humidity_2m": 87,
    "apparent_temperature": 6.1,
    "weather_code": 3,
    "wind_speed_10m": 11.2,
    "wind_direction_10m": 225
  },
  "current_units": {
    "temperature_2m": "°C",
    "relative_humidity_2m": "%",
    "wind_speed_10m": "km/h",
    "wind_direction_10m": "°"
  },
  "daily": {
    "time": ["2024-01-15", "2024-01-16", "2024-01-17"],
    "temperature_2m_max": [10.2, 9.8, 12.1],
    "temperature_2m_min": [5.8, 4.2, 7.3],
    "weather_code": [3, 61, 2],
    "precipitation_sum": [0.0, 2.4, 0.1]
  },
  "daily_units": {
    "temperature_2m_max": "°C",
    "precipitation_sum": "mm"
  }
}

Key observations: Current conditions are in a current object with named fields. Daily forecasts use parallel arrays (each array contains values for multiple days in sequence). Units are separated into their own objects. This structure requires different access patterns than geocoding responses.

Documenting Your Findings

Translate documentation analysis into implementation notes that guide your coding. This step prevents constantly switching between documentation and code editor.

Implementation Specification
Geocoding Implementation Notes
# Geocoding API Implementation
# Endpoint: https://geocoding-api.open-meteo.com/v1/search
# 
# Required parameters:
#   name: city name string (min 2 chars)
#
# Recommended parameters:
#   count: 5 (limit results)
#   language: "en"
#   format: "json"
#
# Response structure:
#   data["results"][0]["latitude"]  -> float
#   data["results"][0]["longitude"] -> float
#   data["results"][0]["name"]      -> string
#   data["results"][0]["admin1"]    -> string (may be absent)
#   data["results"][0]["country"]   -> string
#
# Error cases:
#   - Empty results array if city not found
#   - Network errors (timeout, connection)
#   - Malformed responses (missing keys)
Weather API Implementation Notes
# Weather Forecast API Implementation
# Endpoint: https://api.open-meteo.com/v1/forecast
#
# Required parameters:
#   latitude: float from geocoding
#   longitude: float from geocoding
#
# Weather data parameters:
#   current: ["temperature_2m", "relative_humidity_2m", 
#            "apparent_temperature", "weather_code",
#            "wind_speed_10m", "wind_direction_10m"]
#   daily: ["temperature_2m_max", "temperature_2m_min",
#          "weather_code", "precipitation_sum"]
#
# Configuration:
#   timezone: "auto"
#   forecast_days: 5
#
# Response structure:
#   data["current"]["temperature_2m"] -> float
#   data["current_units"]["temperature_2m"] -> "°C"
#   data["daily"]["time"] -> ["2024-01-15", "2024-01-16", ...]
#   data["daily"]["temperature_2m_max"] -> [10.2, 9.8, ...]
#
# Weather codes:
#   0: Clear sky, 1-3: Cloudy, 61-65: Rain, 71-75: Snow
#   Need lookup table for user-friendly descriptions

These notes capture everything needed for implementation: endpoints, parameters, response navigation paths, and edge cases to handle. With this specification complete, you can build working code without repeatedly consulting documentation.

Takeaways & Next Step

Systematic Documentation Analysis:

  • Four critical questions: What endpoint? What parameters? What response structure? What errors?
  • Focus on your use case: Identify minimum parameters needed rather than learning every option
  • Study example responses: Response structure determines data extraction code
  • Document your findings: Create implementation notes that prevent constant context switching

With API requirements documented, Section 4 implements the geocoding layer using the specification you've created, establishing patterns that will apply to the weather layer as well.

4. Building the Geocoding Layer

The geocoding layer converts city names into coordinates that the weather API requires. This is your first complete API integration, establishing patterns you'll reuse throughout the chapter: progressive request building, systematic error handling, and clean interface design. Build this component until it works reliably before moving to weather data.

Professional developers don't write complete solutions immediately. They start with minimal working code, verify it functions, then add features incrementally. This approach makes debugging simpler and helps you understand how each component contributes to the final result.

Note

The Open-Meteo APIs are completely free and do not require authentication. The api_key support you'll see in later chapters is included only to prepare you for working with commercial or rate-limited APIs that do require credentials. For Open-Meteo, you can safely leave this field unset.

Phase 1: Minimal Working Request

Start with the absolute minimum: endpoint, required parameters, and basic response inspection. This confirms connectivity and helps you understand the response structure before adding complexity.

Minimum Viable Geocoding Request
Python
import requests

# From documentation analysis
geocoding_url = "https://geocoding-api.open-meteo.com/v1/search"

# Minimum required parameter
city_name = "Dublin"
params = {
    "name": city_name
}

# Make the request
response = requests.get(geocoding_url, params=params, timeout=10)
print(f"Status code: {response.status_code}")

# Parse JSON response
data = response.json()
print(f"Response type: {type(data)}")
print(f"Top-level keys: {list(data.keys())}")

# Examine results structure
if "results" in data and data["results"]:
    first_result = data["results"][0]
    print(f"First result keys: {list(first_result.keys())}")
    print(f"\nName: {first_result['name']}")
    print(f"Latitude: {first_result['latitude']}")
    print(f"Longitude: {first_result['longitude']}")
Output
Status code: 200
Response type: <class 'dict'>
Top-level keys: ['results', 'generationtime_ms']
First result keys: ['id', 'name', 'latitude', 'longitude', 'elevation', 'feature_code', 'country_code', 'admin1_id', 'timezone', 'population', 'country', 'admin1']

Name: Dublin
Latitude: 53.33306
Longitude: -6.24889
What This Confirms
  • Request works: 200 status code indicates successful response
  • Structure matches documentation: Results array contains location dictionaries
  • Required fields present: latitude, longitude, and name are available
  • Additional context available: country and admin1 fields provide location details

Phase 2: Enhanced Request with Optional Parameters

The basic request works but returns many results and lacks explicit configuration. Add the optional parameters identified in documentation analysis to improve result quality and make behavior predictable.

Optimized Geocoding Request
Python
import requests

geocoding_url = "https://geocoding-api.open-meteo.com/v1/search"
city_name = "London"

# Enhanced parameters from documentation analysis
params = {
    "name": city_name,
    "count": 5,        # Limit to 5 results instead of default 10
    "language": "en",  # Explicit English responses
    "format": "json"   # Explicit format specification
}

print(f"Searching for '{city_name}' with enhanced parameters...")

response = requests.get(geocoding_url, params=params, timeout=10)
response.raise_for_status()  # Raises exception for HTTP errors

data = response.json()
print(f"Found {len(data['results'])} location(s):\n")

# Display all matches to understand ambiguity
for i, result in enumerate(data["results"], 1):
    name = result.get("name", "Unknown")
    country = result.get("country", "Unknown")
    admin1 = result.get("admin1", "")
    lat = result.get("latitude")
    lon = result.get("longitude")
    
    location = f"{name}, {admin1}, {country}" if admin1 else f"{name}, {country}"
    print(f"{i}. {location}")
    print(f"   Coordinates: ({lat}, {lon})\n")
Output
Searching for 'London' with enhanced parameters...
Found 5 location(s):

1. London, England, United Kingdom
   Coordinates: (51.50853, -0.12574)

2. London, Ontario, Canada
   Coordinates: (42.98339, -81.23304)

3. London, Kentucky, United States
   Coordinates: (37.12898, -84.08326)

4. London, Ohio, United States
   Coordinates: (39.88645, -83.44825)

5. London, Arkansas, United States
   Coordinates: (35.32897, -93.25296)

The enhanced request reveals an important reality: common city names have multiple matches. Professional geocoding implementations either use the first (most populous/relevant) result automatically or let users choose. For a weather dashboard, using the first result is reasonable since the API ranks by relevance.

Phase 3: Error Handling and Edge Cases

Working requests are only half the solution. Production code handles failures gracefully: network timeouts, invalid city names, empty results, and malformed responses. Each failure mode requires appropriate handling.

Geocoding with Comprehensive Error Handling
Python
import requests

def geocode_location(city_name):
    """
    Convert city name to coordinates with error handling.
    Returns: (latitude, longitude, formatted_name) on success
             (None, None, None) on failure
    """
    # Input validation
    if not city_name or len(city_name.strip()) < 2:
        print("Error: City name must be at least 2 characters")
        return None, None, None
    
    city_name = city_name.strip()
    print(f"Looking up coordinates for '{city_name}'...")
    
    geocoding_url = "https://geocoding-api.open-meteo.com/v1/search"
    params = {
        "name": city_name,
        "count": 5,
        "language": "en",
        "format": "json"
    }
    
    try:
        # Network request with timeout
        response = requests.get(geocoding_url, params=params, timeout=10)
        response.raise_for_status()
        data = response.json()
        
        # Check for results
        if "results" not in data or not data["results"]:
            print(f"No locations found for '{city_name}'")
            print("Try checking spelling or using a more specific name")
            return None, None, None
        
        # Extract first (best) match
        location = data["results"][0]
        latitude = location["latitude"]
        longitude = location["longitude"]
        
        # Build formatted location name
        name = location.get("name", "Unknown")
        admin1 = location.get("admin1", "")
        country = location.get("country", "")
        
        name_parts = [name]
        if admin1:
            name_parts.append(admin1)
        if country:
            name_parts.append(country)
        
        full_name = ", ".join(name_parts)
        print(f"Found: {full_name}")
        
        return latitude, longitude, full_name
        
    except requests.exceptions.Timeout:
        print("Request timed out - please check your internet connection")
        return None, None, None
    except requests.exceptions.RequestException as e:
        print(f"Network error occurred: {e}")
        return None, None, None
    except KeyError as e:
        print(f"Unexpected response format - missing field: {e}")
        return None, None, None

# Test with various inputs
test_cases = [
    "Dublin",           # Valid city
    "Tokyo",            # Valid city  
    "InvalidCity123",  # Doesn't exist
    "",                 # Empty input
    "a"                 # Too short
]

for city in test_cases:
    lat, lon, name = geocode_location(city)
    if lat is not None:
        print(f"Success: {name} at ({lat}, {lon})")
    else:
        print("Lookup failed")
    print("-" * 50)
Sample Output
Looking up coordinates for 'Dublin'...
Found: Dublin, Leinster, Ireland
Success: Dublin, Leinster, Ireland at (53.33306, -6.24889)
--------------------------------------------------
Looking up coordinates for 'Tokyo'...
Found: Tokyo, Tokyo, Japan
Success: Tokyo, Tokyo, Japan at (35.6895, 139.69171)
--------------------------------------------------
Looking up coordinates for 'InvalidCity123'...
No locations found for 'InvalidCity123'
Try checking spelling or using a more specific name
Lookup failed
--------------------------------------------------
Error: City name must be at least 2 characters
Lookup failed
--------------------------------------------------
Error: City name must be at least 2 characters
Lookup failed
--------------------------------------------------
Error Handling Patterns
  • Input validation: Check requirements before making network requests
  • Network errors: Catch timeouts and connection failures with clear messages
  • Empty results: Distinguish "no results" from network failures
  • Malformed responses: Handle missing expected fields gracefully
  • Consistent interface: Always return three-tuple, using None to signal failure

Understanding the Interface Contract

The geocode_location() function establishes an interface contract that makes integration simple. It always returns a three-tuple: (latitude, longitude, formatted_name) on success or (None, None, None) on any failure. This consistency means calling code only needs one check.

Using the Geocoding Interface
Python
def get_weather_for_city(city_name):
    """Demonstrate using the geocoding interface."""
    # Get coordinates
    lat, lon, location_name = geocode_location(city_name)
    
    # Single check handles all failure modes
    if lat is None:
        print("Cannot fetch weather without valid coordinates")
        return None
    
    # If we reach here, coordinates are guaranteed valid
    print(f"\nReady to fetch weather for {location_name}")
    print(f"Coordinates: ({lat}, {lon})")
    
    # Weather API call would go here
    return location_name

# Test the interface
get_weather_for_city("Paris")
Output
Looking up coordinates for 'Paris'...
Found: Paris, Île-de-France, France

Ready to fetch weather for Paris, Île-de-France, France
Coordinates: (48.85341, 2.3488)

This interface design separates concerns cleanly. The geocoding function handles all location lookup complexity including errors. Calling code doesn't need to know about network issues, empty results, or response parsing. It just checks if latitude is None.

Takeaways & Next Step

What You Built in This Section:

  • Progressive building approach: Started with minimum request, added parameters, then error handling - reducing debugging complexity at each step
  • Input validation pattern: Check requirements before network requests to fail fast and save resources
  • Comprehensive error coverage: Network timeouts, empty results, malformed data all handled with appropriate messages
  • Clean interface contract: Three-tuple return (lat, lon, name) or (None, None, None) makes success/failure checking simple
  • Reusable pattern established: This same progression (minimal → enhanced → hardened) applies to any API integration

With the geocoding layer working reliably, Section 5 applies this exact pattern to the weather API, handling its more complex response structure while maintaining the same interface consistency.

5. Building the Weather Layer

The weather layer fetches forecast data using coordinates from geocoding. You'll apply the same progressive building approach: start minimal, verify functionality, add features incrementally. The weather API returns more complex responses than geocoding (nested objects, parallel arrays, separate units information), but the systematic approach handles this complexity methodically.

This section reinforces patterns established in the geocoding layer while introducing new challenges: interpreting weather codes, processing array-based daily forecasts, and handling optional fields that may or may not appear in responses.

Phase 1: Minimal Weather Request

Start with required parameters and one simple data field to confirm connectivity and understand response structure. Use coordinates from a successful geocoding lookup.

Minimum Viable Weather Request
Python
import requests

# From documentation analysis
weather_url = "https://api.open-meteo.com/v1/forecast"

# Test coordinates (Dublin from geocoding)
latitude = 53.33306
longitude = -6.24889

# Minimum required parameters plus one data field
params = {
    "latitude": latitude,
    "longitude": longitude,
    "current": ["temperature_2m"]  # Just temperature to start
}

# Make the request
response = requests.get(weather_url, params=params, timeout=10)
response.raise_for_status()

data = response.json()
print(f"Response type: {type(data)}")
print(f"Top-level keys: {list(data.keys())}")

# Examine current weather structure
current = data["current"]
print(f"\nCurrent weather data: {current}")

# Check units information
units = data["current_units"]
print(f"Units: {units}")
Output
Response type: <class 'dict'>
Top-level keys: ['latitude', 'longitude', 'generationtime_ms', 'utc_offset_seconds', 'timezone', 'timezone_abbreviation', 'elevation', 'current_units', 'current']

Current weather data: {'time': '2024-01-15T14:00', 'interval': 900, 'temperature_2m': 8.3}
Units: {'time': 'iso8601', 'interval': 'seconds', 'temperature_2m': '°C'}
Phase 1 Insights
  • Request works: API accepts latitude/longitude and current parameter array
  • Nested structure: Current data lives in separate object from units
  • Timestamp included: Each reading has associated time
  • Units separated: Unit information provided in parallel structure

Phase 2: Complete Current Weather Data

Add all current weather parameters identified during documentation analysis. This provides comprehensive current conditions for display.

Enhanced Current Weather Request
Python
import requests

weather_url = "https://api.open-meteo.com/v1/forecast"
latitude = 53.33306
longitude = -6.24889

# Complete current weather parameters
params = {
    "latitude": latitude,
    "longitude": longitude,
    "current": [
        "temperature_2m",
        "relative_humidity_2m",
        "apparent_temperature",
        "weather_code",
        "wind_speed_10m",
        "wind_direction_10m"
    ]
}

print("Requesting complete current weather data...")
response = requests.get(weather_url, params=params, timeout=10)
response.raise_for_status()

data = response.json()
current = data["current"]
units = data["current_units"]

print("Current weather data:")
for field, value in current.items():
    if field == "time":
        print(f"  {field}: {value}")
    else:
        unit = units.get(field, "")
        print(f"  {field}: {value} {unit}")
Output
Requesting complete current weather data...
Current weather data:
  time: 2024-01-15T14:00
  interval: 900 seconds
  temperature_2m: 8.3 °C
  relative_humidity_2m: 87 %
  apparent_temperature: 6.1 °C
  weather_code: 3 
  wind_speed_10m: 11.2 km/h
  wind_direction_10m: 225 °

The enhanced request provides all data needed for current conditions display. The weather_code field (3 in this example) requires interpretation. You'll build a lookup table for converting numeric codes to readable descriptions.

Phase 3: Adding Daily Forecast Data

Daily forecasts use a different structure than current weather: parallel arrays where each index represents one day. Understanding this structure is critical for correct data extraction.

Complete Weather Request with Daily Forecasts
Python
import requests

weather_url = "https://api.open-meteo.com/v1/forecast"
latitude = 53.33306
longitude = -6.24889

# Combined current + daily parameters
params = {
    "latitude": latitude,
    "longitude": longitude,
    
    # Current weather
    "current": [
        "temperature_2m",
        "relative_humidity_2m",
        "apparent_temperature",
        "weather_code",
        "wind_speed_10m",
        "wind_direction_10m"
    ],
    
    # Daily forecast
    "daily": [
        "temperature_2m_max",
        "temperature_2m_min",
        "weather_code",
        "precipitation_sum",
        "wind_speed_10m_max"
    ],
    
    # Configuration
    "timezone": "auto",
    "forecast_days": 5
}

print("Requesting current weather + daily forecast...")
response = requests.get(weather_url, params=params, timeout=15)
response.raise_for_status()

data = response.json()

# Display current conditions summary
print("\nCurrent conditions:")
current = data["current"]
temp = current["temperature_2m"]
humidity = current["relative_humidity_2m"]
print(f"  Temperature: {temp}°C")
print(f"  Humidity: {humidity}%")

# Display daily forecast structure
print(f"\nDaily forecast:")
daily = data["daily"]
daily_units = data["daily_units"]

print(f"  Number of days: {len(daily['time'])}")
print(f"  Date array: {daily['time']}")

# Show first day's data
print(f"\n  First day details:")
print(f"    Date: {daily['time'][0]}")
print(f"    High: {daily['temperature_2m_max'][0]}{daily_units['temperature_2m_max']}")
print(f"    Low: {daily['temperature_2m_min'][0]}{daily_units['temperature_2m_min']}")
print(f"    Precipitation: {daily['precipitation_sum'][0]}{daily_units['precipitation_sum']}")
Output
Requesting current weather + daily forecast...

Current conditions:
  Temperature: 8.3°C
  Humidity: 87%

Daily forecast:
  Number of days: 5
  Date array: ['2024-01-15', '2024-01-16', '2024-01-17', '2024-01-18', '2024-01-19']

  First day details:
    Date: 2024-01-15
    High: 10.2°C
    Low: 5.8°C
    Precipitation: 0.0mm
Understanding Daily Data Arrays

Daily forecasts use parallel arrays aligned by index. daily['time'][0] corresponds to daily['temperature_2m_max'][0] and daily['precipitation_sum'][0]. All arrays have the same length (5 days in this case). Access data by iterating through indices, not by field names.

Interpreting Weather Codes

The weather API returns conditions as numeric codes following World Meteorological Organization (WMO) standards. Convert these codes to user-friendly descriptions with a lookup function.

Weather Code Interpreter
Python
def interpret_weather_code(code):
    """
    Convert WMO weather code to readable description.
    
    Args:
        code: Integer weather code from API
        
    Returns:
        String description of weather conditions
    """
    weather_codes = {
        # Clear conditions
        0: "Clear sky",
        1: "Mainly clear",
        2: "Partly cloudy",
        3: "Overcast",
        
        # Fog
        45: "Fog",
        48: "Depositing rime fog",
        
        # Drizzle
        51: "Light drizzle",
        53: "Moderate drizzle",
        55: "Dense drizzle",
        
        # Rain
        61: "Slight rain",
        63: "Moderate rain",
        65: "Heavy rain",
        
        # Snow
        71: "Slight snow",
        73: "Moderate snow",
        75: "Heavy snow",
        
        # Rain showers
        80: "Slight rain showers",
        81: "Moderate rain showers",
        82: "Violent rain showers",
        
        # Snow showers
        85: "Slight snow showers",
        86: "Heavy snow showers",
        
        # Thunderstorm
        95: "Thunderstorm",
        96: "Thunderstorm with slight hail",
        99: "Thunderstorm with heavy hail"
    }
    
    return weather_codes.get(code, f"Unknown weather code: {code}")

# Test weather code interpretation
test_codes = [0, 3, 61, 71, 95]

print("Weather code interpretations:")
for code in test_codes:
    description = interpret_weather_code(code)
    print(f"  Code {code}: {description}")
Output
Weather code interpretations:
  Code 0: Clear sky
  Code 3: Overcast
  Code 61: Slight rain
  Code 71: Slight snow
  Code 95: Thunderstorm

This lookup table transforms technical codes into descriptions users understand. The function handles missing codes gracefully by returning a descriptive error message rather than crashing.

Complete Weather Function with Error Handling

Combine all components into a production-ready function following the same interface pattern as geocoding: return data on success, None on failure.

Production Weather Data Function
Python
import requests

def get_weather_data(latitude, longitude, days=5):
    """
    Fetch comprehensive weather data for coordinates.
    
    Args:
        latitude: Geographic latitude from geocoding
        longitude: Geographic longitude from geocoding
        days: Number of forecast days (1-16, default: 5)
    
    Returns:
        Dictionary with weather data on success
        None on failure
    """
    print(f"Fetching {days}-day weather forecast...")
    
    weather_url = "https://api.open-meteo.com/v1/forecast"
    
    params = {
        "latitude": latitude,
        "longitude": longitude,
        
        # Current weather - comprehensive conditions
        "current": [
            "temperature_2m",
            "relative_humidity_2m",
            "apparent_temperature",
            "weather_code",
            "wind_speed_10m",
            "wind_direction_10m"
        ],
        
        # Daily forecast - planning data
        "daily": [
            "temperature_2m_max",
            "temperature_2m_min",
            "weather_code",
            "precipitation_sum",
            "wind_speed_10m_max"
        ],
        
        # Configuration
        "timezone": "auto",
        "forecast_days": days
    }
    
    try:
        response = requests.get(weather_url, params=params, timeout=15)
        response.raise_for_status()
        
        data = response.json()
        print("Weather data retrieved successfully")
        return data
        
    except requests.exceptions.Timeout:
        print("Weather request timed out - check internet connection")
        return None
    except requests.exceptions.RequestException as e:
        print(f"Weather request failed: {e}")
        return None

# Test the complete weather function
test_locations = [
    (53.33306, -6.24889, "Dublin"),
    (35.6895, 139.69171, "Tokyo")
]

for lat, lon, name in test_locations:
    print(f"\nTesting weather forecast for {name}:")
    weather_data = get_weather_data(lat, lon, days=3)
    
    if weather_data:
        # Display current conditions
        current = weather_data["current"]
        temp = current["temperature_2m"]
        code = current["weather_code"]
        conditions = interpret_weather_code(code)
        
        print(f"  Current: {temp}°C, {conditions}")
        
        # Display forecast summary
        daily = weather_data["daily"]
        num_days = len(daily["time"])
        max_temps = daily["temperature_2m_max"]
        min_temps = daily["temperature_2m_min"]
        
        avg_high = sum(max_temps) / len(max_temps)
        avg_low = sum(min_temps) / len(min_temps)
        
        print(f"  Forecast: {num_days} days, avg {avg_low:.1f}°C to {avg_high:.1f}°C")
    else:
        print("  Weather forecast failed")
    
    print("-" * 50)
Sample Output
Testing weather forecast for Dublin:
Fetching 3-day weather forecast...
Weather data retrieved successfully
  Current: 8.3°C, Overcast
  Forecast: 3 days, avg 5.4°C to 9.7°C
--------------------------------------------------

Testing weather forecast for Tokyo:
Fetching 3-day weather forecast...
Weather data retrieved successfully
  Current: 7.2°C, Clear sky
  Forecast: 3 days, avg 3.2°C to 11.8°C
--------------------------------------------------
Interface Consistency
  • Same pattern as geocoding: Returns data or None, never raises exceptions to caller
  • Complete data structure: Returns entire response dictionary for flexible downstream use
  • Clear success signal: Calling code checks if weather_data: once
  • User feedback: Prints progress and error messages for visibility

Takeaways & Next Step

Weather Layer Complete:

  • Same progressive approach: Minimum request → enhanced parameters → error handling
  • Complex response handling: Nested objects, parallel arrays, separated units all processed correctly
  • Weather code interpretation: Numeric codes converted to user-friendly descriptions
  • Consistent interface: Returns data dictionary or None, matching geocoding pattern
  • Configurable forecasts: Days parameter allows flexibility in forecast length

With both API layers working independently, Section 6 builds the integration logic that coordinates geocoding and weather requests into a unified workflow, handling the cascading dependencies and potential failures at each step.

6. Coordinating Multiple APIs

You've built two independent API layers that work reliably: geocoding converts city names to coordinates, weather fetches forecast data for coordinates. Now you'll integrate them into a coordinated workflow that manages the sequential dependency between services, validates data at each step, and handles failures gracefully.

Integration is where multi-API applications succeed or fail. The challenge isn't calling APIs. You've already done that. The challenge is orchestration: ensuring step 2 only runs when step 1 succeeds, transforming data between layers correctly, providing clear feedback when any step fails, and maintaining clean code organization as complexity increases.

The Naive Approach and Its Problems

A direct implementation calls each API sequentially and hopes both succeed. This works under ideal conditions but fails unprofessionally when problems occur.

Simple Sequential Calls (Problematic)
Python
def get_weather_simple(city_name):
    """Naive approach - works until it doesn't."""
    # Call geocoding
    lat, lon, location = geocode_location(city_name)
    
    # Call weather - assumes geocoding succeeded
    weather = get_weather_data(lat, lon)
    
    # Display - assumes weather succeeded
    print(f"Weather for {location}:")
    print(f"Temperature: {weather['current']['temperature_2m']}°C")
    
# This crashes when geocoding fails
get_weather_simple("InvalidCity123")
Error Output
Looking up coordinates for 'InvalidCity123'...
No locations found for 'InvalidCity123'
Try checking spelling or using a more specific name
Fetching 5-day weather forecast...
Traceback (most recent call last):
  File "weather.py", line 8, in get_weather_simple
    weather = get_weather_data(lat, lon)
TypeError: get_weather_data() argument after ** must be a mapping, not NoneType

The function crashes because it passes None values to get_weather_data() when geocoding fails. Users see technical error messages instead of helpful guidance. The problem compounds with each additional API: more integration points mean more failure modes.

Professional Integration Pattern

Professional integration validates each step before proceeding to the next. This fail-fast approach stops execution when problems occur and provides clear feedback about what went wrong.

Validated Sequential Integration
Python
def get_weather_for_city(city_name):
    """
    Coordinated workflow with validation at each step.
    
    Args:
        city_name: City name to look up
        
    Returns:
        Tuple of (weather_data, location_name) on success
        (None, None) on any failure
    """
    # Step 1: Geocoding - get coordinates
    lat, lon, location_name = geocode_location(city_name)
    
    # Validate step 1 before proceeding
    if lat is None:
        print("Cannot get weather without valid location coordinates")
        return None, None
    
    # Step 2: Weather - get forecast data
    weather_data = get_weather_data(lat, lon)
    
    # Validate step 2 before proceeding
    if weather_data is None:
        print("Cannot display weather without valid data")
        return None, None
    
    # Both steps succeeded - return data
    return weather_data, location_name

# Test with various inputs
test_cities = ["Dublin", "Tokyo", "InvalidCity123"]

for city in test_cities:
    print(f"\n{'='*60}")
    print(f"Looking up weather for: {city}")
    print('='*60)
    
    weather, location = get_weather_for_city(city)
    
    if weather is not None:
        # Safe to access weather data
        current = weather["current"]
        temp = current["temperature_2m"]
        code = current["weather_code"]
        conditions = interpret_weather_code(code)
        
        print(f"\n✓ Success: {location}")
        print(f"  Current: {temp}°C, {conditions}")
    else:
        print("\n✗ Weather lookup failed")
Output
============================================================
Looking up weather for: Dublin
============================================================
Looking up coordinates for 'Dublin'...
Found: Dublin, Leinster, Ireland
Fetching 5-day weather forecast...
Weather data retrieved successfully

✓ Success: Dublin, Leinster, Ireland
  Current: 8.3°C, Overcast

============================================================
Looking up weather for: Tokyo
============================================================
Looking up coordinates for 'Tokyo'...
Found: Tokyo, Tokyo, Japan
Fetching 5-day weather forecast...
Weather data retrieved successfully

✓ Success: Tokyo, Tokyo, Japan
  Current: 7.2°C, Clear sky

============================================================
Looking up weather for: InvalidCity123
============================================================
Looking up coordinates for 'InvalidCity123'...
No locations found for 'InvalidCity123'
Try checking spelling or using a more specific name
Cannot get weather without valid location coordinates

✗ Weather lookup failed
What Professional Integration Provides
  • Validation gates: Each step checks results before continuing
  • Early termination: Stops immediately when a step fails rather than cascading errors
  • Clear feedback: Users understand what failed and why
  • Safe data access: Calling code never receives None in places expecting dictionaries
  • Consistent interface: Returns data tuple or (None, None) just like component layers

Understanding Data Flow

The integration function manages data transformation between layers. Geocoding output becomes weather input. Weather output becomes display input. Each transformation includes validation.

Data Flow Diagram
Data Transformation Steps
# Step 1: User Input → Geocoding Layer
city_name: "Dublin"  (string)
    ↓
geocode_location(city_name)
    ↓
(53.33306, -6.24889, "Dublin, Leinster, Ireland")  (lat, lon, name)

# Validation checkpoint
if lat is None:
    return None, None  # Stop here

# Step 2: Coordinates → Weather Layer  
lat: 53.33306  (float)
lon: -6.24889  (float)
    ↓
get_weather_data(lat, lon)
    ↓
{current: {...}, daily: {...}, ...}  (dictionary)

# Validation checkpoint
if weather_data is None:
    return None, None  # Stop here

# Step 3: Weather Data → Display Layer
weather_data: {current: {...}}  (dictionary)
location_name: "Dublin, Leinster, Ireland"  (string)
    ↓
return (weather_data, location_name)  # Success

Each arrow represents a transformation with validation. Data flows forward only when validation passes. This creates a pipeline where failure at any step prevents downstream processing.

Handling Partial Failures

The integration function currently treats any failure as complete failure. For some applications, you might want to distinguish between different failure types and provide more specific guidance.

Enhanced Error Handling with Failure Types
Python
def get_weather_with_diagnostics(city_name):
    """
    Integration with detailed failure reporting.
    
    Returns:
        (weather_data, location_name, None) on success
        (None, None, error_type) on failure
    """
    # Step 1: Geocoding
    lat, lon, location_name = geocode_location(city_name)
    
    if lat is None:
        # Geocoding failed - different causes possible
        if not city_name or len(city_name.strip()) < 2:
            return None, None, "INVALID_INPUT"
        else:
            return None, None, "LOCATION_NOT_FOUND"
    
    # Step 2: Weather
    weather_data = get_weather_data(lat, lon)
    
    if weather_data is None:
        # Weather API failed - likely temporary
        return None, None, "WEATHER_UNAVAILABLE"
    
    # Success
    return weather_data, location_name, None

# Use enhanced error reporting
city = "InvalidCity123"
weather, location, error = get_weather_with_diagnostics(city)

if error:
    if error == "INVALID_INPUT":
        print("Please enter a valid city name (at least 2 characters)")
    elif error == "LOCATION_NOT_FOUND":
        print(f"Could not find '{city}' - check spelling or try a nearby city")
    elif error == "WEATHER_UNAVAILABLE":
        print("Weather service temporarily unavailable - please try again")
else:
    print(f"Weather for {location}: {weather['current']['temperature_2m']}°C")

This enhanced version distinguishes between input errors (user can fix immediately), location errors (might need different city name), and service errors (temporary, retry later). The calling code provides targeted guidance based on failure type.

Reusable Integration Functions

The integration logic you've built is reusable across different interfaces. Whether displaying in a terminal, web page, or mobile app, the same integration function coordinates the APIs reliably.

Separation of Integration and Presentation
Python
def get_weather_for_city(city_name):
    """Core integration logic - no display code."""
    lat, lon, location_name = geocode_location(city_name)
    if lat is None:
        return None, None
    
    weather_data = get_weather_data(lat, lon)
    if weather_data is None:
        return None, None
    
    return weather_data, location_name

def display_terminal(city_name):
    """Terminal interface using core integration."""
    weather, location = get_weather_for_city(city_name)
    if weather:
        print(f"Weather for {location}:")
        print(f"Temp: {weather['current']['temperature_2m']}°C")

def get_json_response(city_name):
    """API endpoint using core integration."""
    weather, location = get_weather_for_city(city_name)
    if weather:
        return {
            "location": location,
            "temperature": weather['current']['temperature_2m'],
            "status": "success"
        }
    else:
        return {"status": "error", "message": "Weather unavailable"}

def display_gui(city_name):
    """GUI using core integration."""
    weather, location = get_weather_for_city(city_name)
    # Update GUI elements with weather data
    # Implementation depends on GUI framework
    pass

The integration function remains unchanged across all these use cases. Only the presentation layer varies. This separation makes code easier to test (test integration independently from display) and more maintainable (change display without touching integration logic).

Takeaways & Next Step

Multi-API Coordination Complete:

  • Validation gates: Check each step's results before proceeding to prevent cascading failures
  • Data transformation: Manage conversion between layers explicitly with clear validation
  • Fail-fast approach: Stop immediately when problems occur rather than propagating errors
  • Clear error reporting: Distinguish failure types for targeted user guidance
  • Separation of concerns: Integration logic independent from presentation layer

With reliable integration established, Section 7 builds the complete weather dashboard application: class-based organization, professional display formatting, interactive user interface, and production-ready error handling.

7. Building the Complete Dashboard

You've built three working components: geocoding layer, weather layer, and integration logic. Now you'll organize them into a complete application with professional structure, polished display formatting, and interactive user interface. This section demonstrates how individual functions evolve into cohesive applications through class-based organization and careful attention to user experience.

The weather dashboard will run as an interactive command-line application where users enter city names and receive formatted forecasts. The architecture separates data retrieval from presentation, making the code testable and maintainable. Each component has clear responsibilities and clean interfaces.

Class-Based Organization

Classes provide natural organization for related functions and shared state. The WeatherDashboard class encapsulates all API endpoints, helper functions, and display methods in one cohesive unit.

Dashboard Class Structure
Python
import requests
from datetime import datetime

class WeatherDashboard:
    """Complete weather dashboard with geocoding and forecast display."""
    
    def __init__(self):
        """Initialize dashboard with API endpoints."""
        self.geocoding_url = "https://geocoding-api.open-meteo.com/v1/search"
        self.weather_url = "https://api.open-meteo.com/v1/forecast"
    
    def find_location(self, city_name):
        """Convert city name to coordinates."""
        # Implementation from Section 4
        pass
    
    def get_weather_data(self, latitude, longitude):
        """Fetch weather data for coordinates."""
        # Implementation from Section 5
        pass
    
    def interpret_weather_code(self, code):
        """Convert weather code to description."""
        # Implementation from Section 5
        pass
    
    def display_current_weather(self, weather_data, location_name):
        """Display current conditions."""
        # New formatting methods in this section
        pass
    
    def display_forecast(self, weather_data):
        """Display daily forecast."""
        # New formatting methods in this section
        pass
    
    def get_weather_for_city(self, city_name):
        """Coordinate geocoding and weather lookup."""
        # Integration logic from Section 6
        pass
    
    def run_interactive_session(self):
        """Run interactive dashboard session."""
        # Interactive interface in this section
        pass

This structure groups related functionality logically. API calls (find_location, get_weather_data) are separate from display methods (display_current_weather, display_forecast). The integration method (get_weather_for_city) coordinates them. The interactive interface (run_interactive_session) manages user interaction.

Utility Methods for Data Formatting

Before building display methods, add utility functions that format raw data into user-friendly text. These helpers keep display code clean by handling formatting details separately.

Formatting Helper Methods
Python
class WeatherDashboard:
    # ... previous methods ...
    
    def get_wind_direction(self, degrees):
        """
        Convert wind direction degrees to compass direction.
        
        Args:
            degrees: Wind direction in degrees (0-360)
            
        Returns:
            Compass direction string (N, NE, E, etc.)
        """
        directions = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
                     "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
        index = round(degrees / 22.5) % 16
        return directions[index]
    
    def format_timestamp(self, timestamp):
        """
        Format ISO timestamp for display.
        
        Args:
            timestamp: ISO format timestamp string
            
        Returns:
            Formatted time string
        """
        try:
            dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
            return dt.strftime("%H:%M on %B %d, %Y")
        except (ValueError, AttributeError):
            return timestamp
    
    def format_date(self, date_string):
        """
        Format date string with smart labels.
        
        Args:
            date_string: ISO format date string (YYYY-MM-DD)
            
        Returns:
            Day name or "Today"/"Tomorrow"
        """
        try:
            dt = datetime.fromisoformat(date_string)
            today = datetime.now().date()
            date_obj = dt.date()
            
            if date_obj == today:
                return "Today"
            elif (date_obj - today).days == 1:
                return "Tomorrow"
            else:
                return dt.strftime("%A")
        except (ValueError, AttributeError):
            return date_string

These utilities transform technical data into readable formats. Wind direction converts 225° to "SW". Timestamps become "14:30 on January 15, 2024". Dates show "Today", "Tomorrow", or day names. Display methods call these helpers rather than handling formatting inline.

Current Weather Display

The current weather display shows real-time conditions in a formatted, easy-to-scan layout. It uses the raw weather data dictionary returned by get_weather_data().

Current Conditions Formatter
Python
class WeatherDashboard:
    # ... previous methods ...
    
    def display_current_weather(self, weather_data, location_name):
        """
        Display current weather conditions in formatted layout.
        
        Args:
            weather_data: Complete weather response dictionary
            location_name: Formatted location name from geocoding
        """
        current = weather_data["current"]
        current_units = weather_data["current_units"]
        
        # Header
        print(f"\n🌤️  Current Weather for {location_name}")
        print("=" * (len(location_name) + 25))
        
        # Temperature
        temp = current.get("temperature_2m")
        temp_unit = current_units.get("temperature_2m", "°C")
        if temp is not None:
            print(f"🌡️  Temperature: {temp}{temp_unit}")
            
            # Show "feels like" if significantly different
            apparent_temp = current.get("apparent_temperature")
            if apparent_temp is not None and abs(apparent_temp - temp) > 2:
                print(f"    Feels like: {apparent_temp}{temp_unit}")
        
        # Weather conditions
        weather_code = current.get("weather_code")
        if weather_code is not None:
            conditions = self.interpret_weather_code(weather_code)
            print(f"☁️  Conditions: {conditions}")
        
        # Humidity
        humidity = current.get("relative_humidity_2m")
        if humidity is not None:
            print(f"💧 Humidity: {humidity}%")
        
        # Wind information
        wind_speed = current.get("wind_speed_10m")
        wind_direction = current.get("wind_direction_10m")
        wind_unit = current_units.get("wind_speed_10m", "km/h")
        
        if wind_speed is not None:
            wind_info = f"💨 Wind: {wind_speed} {wind_unit}"
            if wind_direction is not None:
                compass = self.get_wind_direction(wind_direction)
                wind_info += f" from {compass}"
            print(wind_info)
        
        # Update timestamp
        timestamp = current.get("time")
        if timestamp:
            formatted_time = self.format_timestamp(timestamp)
            print(f"🕐 Updated: {formatted_time}")
Example Output
🌤️  Current Weather for Dublin, Leinster, Ireland
====================================================
🌡️  Temperature: 8.3°C
    Feels like: 6.1°C
☁️  Conditions: Overcast
💧 Humidity: 87%
💨 Wind: 11.2 km/h from SW
🕐 Updated: 14:00 on January 15, 2024

The display method uses .get() throughout to handle missing fields gracefully. If humidity data is absent, that line simply doesn't print. The "feels like" temperature only appears when it differs meaningfully from actual temperature. This defensive approach prevents crashes from incomplete API responses.

Daily Forecast Display

The forecast display processes the parallel arrays in the daily data structure, creating a readable table showing temperature ranges, conditions, and precipitation for each day.

Daily Forecast Formatter
Python
class WeatherDashboard:
    # ... previous methods ...
    
    def display_forecast(self, weather_data):
        """
        Display 5-day weather forecast in tabular format.
        
        Args:
            weather_data: Complete weather response dictionary
        """
        daily = weather_data["daily"]
        daily_units = weather_data["daily_units"]
        
        print(f"\n📅 5-Day Forecast")
        print("=" * 50)
        
        # Extract arrays
        dates = daily.get("time", [])
        max_temps = daily.get("temperature_2m_max", [])
        min_temps = daily.get("temperature_2m_min", [])
        weather_codes = daily.get("weather_code", [])
        precipitation = daily.get("precipitation_sum", [])
        
        temp_unit = daily_units.get("temperature_2m_max", "°C")
        precip_unit = daily_units.get("precipitation_sum", "mm")
        
        # Display each day
        for i in range(min(len(dates), 5)):
            date = dates[i]
            day_name = self.format_date(date)
            
            # Temperature range
            temp_range = "Temperature: "
            if i < len(min_temps) and min_temps[i] is not None:
                temp_range += f"{min_temps[i]}{temp_unit}"
            else:
                temp_range += "N/A"
            
            temp_range += " to "
            if i < len(max_temps) and max_temps[i] is not None:
                temp_range += f"{max_temps[i]}{temp_unit}"
            else:
                temp_range += "N/A"
            
            # Weather conditions
            conditions = "Clear"
            if i < len(weather_codes) and weather_codes[i] is not None:
                conditions = self.interpret_weather_code(weather_codes[i])
            
            # Precipitation (if any)
            precip_info = ""
            if i < len(precipitation) and precipitation[i] is not None and precipitation[i] > 0:
                precip_info = f" | Rain: {precipitation[i]}{precip_unit}"
            
            # Print formatted row
            print(f"{day_name:12} | {temp_range:25} | {conditions}{precip_info}")
Example Output
📅 5-Day Forecast
==================================================
Today        | Temperature: 5.8°C to 10.2°C | Overcast
Tomorrow     | Temperature: 4.2°C to 9.8°C  | Slight rain | Rain: 2.4mm
Wednesday    | Temperature: 7.3°C to 12.1°C | Partly cloudy
Thursday     | Temperature: 6.1°C to 11.5°C | Mainly clear
Friday       | Temperature: 5.4°C to 10.8°C | Clear sky

The forecast display handles array alignment carefully. Each index corresponds to one day across all arrays. Bounds checking prevents crashes if arrays are different lengths. Missing data shows as "N/A" rather than causing errors.

Integration Method with Display

The get_weather_for_city() method now calls display functions after successful data retrieval. This combines integration logic from Section 6 with the formatting methods built in this section.

Complete Integration with Display
Python
class WeatherDashboard:
    # ... previous methods ...
    
    def get_weather_for_city(self, city_name):
        """
        Complete workflow: lookup location, fetch weather, display results.
        
        Args:
            city_name: City name to look up
            
        Returns:
            True if successful, False if any step failed
        """
        # Step 1: Geocoding
        latitude, longitude, location_name = self.find_location(city_name)
        
        if latitude is None:
            print("Cannot get weather without valid location coordinates")
            return False
        
        # Step 2: Weather data
        weather_data = self.get_weather_data(latitude, longitude)
        
        if weather_data is None:
            print("Cannot display weather without valid data")
            return False
        
        # Step 3: Display results
        self.display_current_weather(weather_data, location_name)
        self.display_forecast(weather_data)
        
        return True

The method returns a boolean success indicator. This simple interface makes it easy to check whether the complete workflow succeeded without examining intermediate results. Calling code can use this for flow control or status reporting.

Interactive Session Interface

The interactive interface creates a continuous loop where users enter city names and receive forecasts. It handles user commands (quit/exit), validates input, and provides clear feedback for all outcomes.

Interactive Dashboard Session
Python
class WeatherDashboard:
    # ... previous methods ...
    
    def run_interactive_session(self):
        """
        Run interactive weather dashboard session.
        Continues until user quits.
        """
        print("🌦️  Weather Dashboard")
        print("=" * 30)
        print("Enter city names to get weather forecasts.")
        print("Type 'quit' or 'exit' to stop.\n")
        
        while True:
            try:
                # Get user input
                city_input = input("Enter city name: ").strip()
                
                # Check for exit commands
                if city_input.lower() in ['quit', 'exit', 'q']:
                    print("\nThanks for using the Weather Dashboard! 👋")
                    break
                
                # Validate input
                if not city_input:
                    print("Please enter a city name.\n")
                    continue
                
                # Get and display weather
                print()  # Add spacing
                success = self.get_weather_for_city(city_input)
                
                if success:
                    print("\n" + "─" * 60)
                else:
                    print("\nPlease try a different city name.")
                    print("─" * 60)
                
                print()  # Add spacing for next input
                
            except KeyboardInterrupt:
                print("\n\nSession ended. Goodbye! 👋")
                break
            except Exception as e:
                print(f"\nUnexpected error: {e}")
                print("Please try again.\n")

# Create and run dashboard
def main():
    """Main entry point for weather dashboard application."""
    dashboard = WeatherDashboard()
    dashboard.run_interactive_session()

if __name__ == "__main__":
    main()
Example Session
🌦️  Weather Dashboard
==============================
Enter city names to get weather forecasts.
Type 'quit' or 'exit' to stop.

Enter city name: Dublin
Looking up coordinates for 'Dublin'...
Found: Dublin, Leinster, Ireland
Fetching 5-day weather forecast...
Weather data retrieved successfully

🌤️  Current Weather for Dublin, Leinster, Ireland
====================================================
🌡️  Temperature: 8.3°C
    Feels like: 6.1°C
☁️  Conditions: Overcast
💧 Humidity: 87%
💨 Wind: 11.2 km/h from SW
🕐 Updated: 14:00 on January 15, 2024

📅 5-Day Forecast
==================================================
Today        | Temperature: 5.8°C to 10.2°C | Overcast
Tomorrow     | Temperature: 4.2°C to 9.8°C  | Slight rain | Rain: 2.4mm
Wednesday    | Temperature: 7.3°C to 12.1°C | Partly cloudy
Thursday     | Temperature: 6.1°C to 11.5°C | Mainly clear
Friday       | Temperature: 5.4°C to 10.8°C | Clear sky

────────────────────────────────────────────────────────────

Enter city name: quit

Thanks for using the Weather Dashboard! 👋
Interactive Interface Features
  • Continuous operation: Loop continues until explicit exit command
  • Multiple exit options: 'quit', 'exit', or 'q' all work
  • Input validation: Empty input prompts for retry rather than crashing
  • Keyboard interrupt handling: Ctrl+C exits gracefully
  • Exception safety: Unexpected errors don't crash the application
  • Clear visual separation: Line separators and spacing improve readability

Takeaways & Next Step

Complete Weather Dashboard Application:

  • Class-based organization: Related methods grouped logically with clear responsibilities
  • Utility methods: Formatting helpers keep display code clean and reusable
  • Professional display: Formatted output with emojis, alignment, defensive field access
  • Interactive interface: Continuous operation with proper input validation and exit handling
  • Separation of concerns: Data retrieval, integration, and presentation are independent layers

Section 8 reflects on what makes this application production-ready, identifies patterns that generalize to other multi-API integrations, and previews advanced topics like caching, rate limiting, and comprehensive error handling that Chapter 9 will address.

8. Chapter Summary

You've built a complete weather dashboard that demonstrates professional multi-API integration. This application coordinates two external services, manages sequential dependencies, handles failures at multiple points, and presents data in a polished, user-friendly interface. The systematic approach you followed (understanding requirements, building components independently, integrating with validation, and adding professional polish) applies to any multi-API project.

The weather dashboard isn't just a working program. It's a template for understanding how professional applications combine different services into cohesive solutions. Each pattern you implemented (progressive request building, clean interface design, validation gates, separation of concerns) appears throughout production software regardless of domain or scale.

Key Skills Mastered

1.

Systematic Documentation Analysis

You learned to extract implementation requirements from API documentation methodically: identifying endpoints, required versus optional parameters, response structures, and error conditions. This skill transfers directly to integrating any unfamiliar API.

2.

Progressive Component Building

Starting with minimal requests and adding features incrementally reduced debugging complexity and helped you understand how each parameter affects behavior. This approach works whether building API layers, user interfaces, or complete applications.

3.

Clean Interface Design

Each layer returns simple, consistent results: coordinates or None, weather data or None, success or failure. This consistency makes integration straightforward and testing simple. Complex internal logic hides behind simple external interfaces.

4.

Comprehensive Error Handling

Input validation, network timeouts, empty results, and malformed responses all receive appropriate handling. Users see helpful messages instead of technical errors. The application continues running rather than crashing on individual failures.

5.

Multi-API Coordination

Validation gates between API calls prevent cascading failures. Data transformation happens explicitly with clear success/failure signals. The integration layer manages complexity while maintaining clean code organization.

6.

Professional Application Architecture

Class-based organization, separation of data retrieval from presentation, utility methods for formatting, and interactive user interface all demonstrate production development practices. The code is maintainable, testable, and extensible.

Chapter Review Quiz

Test your understanding with these comprehensive questions. If you can answer confidently, you've mastered the material:

Select question to reveal the answer:
Why must the geocoding API be called before the weather API?

The weather API requires latitude and longitude as input parameters, which only the geocoding API can provide by converting city names to coordinates. You cannot reverse the order because you would have no coordinates to pass to the weather API. This sequential dependency is fundamental to the integration - weather data depends on location data. The get_weather_for_city() method must first call geocoding to get coordinates, then use those coordinates for the weather request.

Why does get_weather_for_city() validate that latitude is not None?

Without validation, the code would pass None values to get_weather_data(latitude, longitude), causing a TypeError when the function tries to use these values in the API request. The if latitude is None check stops execution immediately when geocoding fails, providing a clear error message ("Could not find location") rather than letting the error cascade into confusing failures in subsequent steps. This is the fail-fast principle from Chapter 4 - catch problems early where they're easiest to diagnose.

Why do the API methods return None on failure instead of raising exceptions?

Both find_location() and get_weather_data() return None on failure rather than raising exceptions. This creates a consistent interface that integration code can check with a simple if result is None: test. Exceptions would require try/except blocks at each integration point, making the code more complex and harder to read. The current design lets each layer handle its own errors internally and signal failure simply, while the coordinator (get_weather_for_city()) focuses on orchestration logic rather than error handling mechanics.

How do parallel arrays work in the weather API response?

The weather API returns daily forecast data as parallel arrays: daily['time'], daily['temperature_2m_max'], and daily['weather_code']. The same index position across all arrays represents the same day. So daily['time'][0], daily['temperature_2m_max'][0], and daily['weather_code'][0] all give you data for the first forecast day. Index 1 is day 2, index 2 is day 3, and so on. This parallel structure means you access data by iterating through indices rather than navigating nested objects. It's a compact format that groups similar data types together while maintaining day-to-day relationships through position.

Why does KeyboardInterrupt need special handling?

Keyboard interrupts (Ctrl+C) are deliberate user actions to stop the program, not errors. Catching KeyboardInterrupt separately from general exceptions allows the application to exit cleanly with a friendly "Goodbye!" message rather than printing an error stack trace. This distinguishes between "user wants to quit" (expected behavior, handle gracefully) and "something went wrong" (unexpected error, show diagnostic details). Without special handling, users would see ugly Python tracebacks when they simply pressed Ctrl+C to exit.

How does separating utility methods improve code maintainability?

Methods like format_date() and interpret_weather_code() are separated from display methods because they have single, focused responsibilities. If date formatting needs to change, you modify one small method instead of hunting through display code. If weather codes need translation to different languages, you update one function. This separation makes testing easier (test formatting independently), changes safer (no risk of breaking display logic when fixing date formats), and code reusable (other parts of the application can use these utilities). It's the Single Responsibility Principle in action - each method does one thing well.

What Makes This Production-Ready

The weather dashboard demonstrates several characteristics that distinguish production applications from learning exercises:

1.

Graceful Degradation

When components fail, the application provides clear feedback and continues running. Users can try different inputs without restarting. Partial failures don't crash the entire system.

2.

User-Focused Error Messages

Technical errors become actionable guidance. "No locations found" is more helpful than "KeyError: 'results'". Users understand what went wrong and what to do next.

3.

Defensive Data Access

Display methods use .get() throughout, handling missing fields gracefully. The application works with incomplete API responses rather than requiring perfect data.

4.

Maintainable Architecture

Clear separation between data retrieval, integration logic, and presentation makes the code easy to modify. Adding new features or changing APIs requires localized changes rather than system-wide rewrites.

Patterns That Generalize

The techniques you practiced with weather APIs apply directly to other integration scenarios:

  • E-commerce platforms: Product search API → Inventory API → Price calculation API, with validation at each step
  • Social media dashboards: Authentication API → Profile API → Posts API, coordinated through integration layer
  • Travel booking: Location search → Availability check → Pricing → Reservation, each dependent on previous success
  • Data pipelines: Extract from API → Transform data → Validate → Load to database, with error handling throughout

The coordination pattern remains consistent: identify dependencies, build components independently, integrate with validation gates, handle failures appropriately. Scale and complexity increase, but fundamental approaches stay the same.

Looking Forward

Your weather dashboard handles failures gracefully: validating input before requests, catching network timeouts, returning None on failures, and showing clear error messages instead of crashes. This foundational error handling keeps the application running and users informed. Production systems build on this foundation with systematic approaches: categorizing errors by type (transient network issues versus permanent invalid input), implementing retry strategies that recover automatically from temporary failures, and composing user-friendly messages that guide users toward success. Chapter 9 introduces these production error handling patterns: you'll categorize failures systematically, implement smart retry logic with exponential backoff and jitter, build user-friendly message composers, and add resilience layers that recover from transient issues while preserving your application's clean interface and existing behavior.

The application also processes JSON responses defensively but doesn't validate data systematically. Chapter 10 covers advanced JSON processing techniques for handling complex nested structures, normalizing data from different API formats, and building flexible accessors that work across response variations. Chapter 11 introduces schema-based validation that catches data quality issues before they reach your application logic.

The patterns you've mastered here (progressive building, clean interfaces, validation gates, separation of concerns) form the foundation for these advanced topics. Each new technique builds on the systematic approach you've practiced with the weather dashboard.

Your Development Journey

Building the weather dashboard marks a significant milestone. You've moved from learning isolated concepts to creating complete applications that integrate multiple systems, handle real-world complexity, and provide genuine utility. The skills you've developed apply broadly across software development, regardless of domain or technology stack. Multi-API integration is fundamental to modern applications, and you now understand both the techniques and the thinking that make it successful.