Project · after Level 5

Weather CLI 🌦️

Your first tool that reaches out to the internet and comes back with something useful: the weather, from a real API, for a city you name on the command line. It ties together HTTP, JSON, argument parsing and secret handling into one genuinely handy program.

Difficulty 🐍🐍🐍🐍🐍

📋 Build this

  • Take a city name as a command-line argument.
  • Fetch current weather from a free API (Open-Meteo needs no key).
  • Print temperature and conditions in a clean, readable line.
  • Handle a city that is not found, and a network failure, without a stack trace.

Hints, if you want them

Try the spec cold first. Open a hint only when you are properly stuck; the struggle is where the learning is.

Hint 1: Parsing the argument
argparse gives you city as a positional argument plus a free --help (Lesson 27). Structure the code so main(argv=None) is testable.
Hint 2: Calling the API
requests.get(url, params={...}, timeout=10), then response.raise_for_status() and response.json() (Lesson 42). Open-Meteo has a geocoding endpoint to turn a city name into coordinates.
Hint 3: Failing gracefully
Catch requests.RequestException for network problems and check whether the geocoding result is empty for an unknown city. Print a friendly message and return a non-zero exit code (Lesson 27).

The reference solution

Yours does not need to match this. There are many good ways to build any of these. Compare only after you have your own working.

Reveal the reference solution
#!/usr/bin/env python3
"""weather: current conditions for a city, from Open-Meteo (no key needed)."""

import argparse
import sys

import requests

GEO = "https://geocoding-api.open-meteo.com/v1/search"
FORECAST = "https://api.open-meteo.com/v1/forecast"


def find_city(name):
    """Turn a city name into (lat, lon, label), or None if not found."""
    resp = requests.get(GEO, params={"name": name, "count": 1}, timeout=10)
    resp.raise_for_status()
    results = resp.json().get("results")
    if not results:
        return None
    hit = results[0]
    return hit["latitude"], hit["longitude"], f"{hit['name']}, {hit['country']}"


def current_weather(lat, lon):
    resp = requests.get(FORECAST, params={
        "latitude": lat, "longitude": lon, "current_weather": True,
    }, timeout=10)
    resp.raise_for_status()
    return resp.json()["current_weather"]


def main(argv=None):
    parser = argparse.ArgumentParser(description="Current weather for a city.")
    parser.add_argument("city", help="the city to look up")
    args = parser.parse_args(argv)

    try:
        located = find_city(args.city)
        if located is None:
            print(f"weather: no city called {args.city!r}", file=sys.stderr)
            return 1
        lat, lon, label = located
        weather = current_weather(lat, lon)
    except requests.RequestException as err:
        print(f"weather: network error: {err}", file=sys.stderr)
        return 1

    print(f"{label}: {weather['temperature']}C, wind {weather['windspeed']} km/h")
    return 0


if __name__ == "__main__":
    sys.exit(main())

This one needs the network or a live secret, so it has no run button. Build it on your own machine.

Stretch goals

+250 XP