Build a Python Weather App from the Command Line
A command-line weather app is a good first Python project because it touches four everyday skills in under 100 lines: parsing command-line arguments, calling an HTTP API, reading JSON, and handling errors cleanly. This app uses Open-Meteo, a free weather API that needs no API key. One call turns a city name into coordinates (geocoding), a second call fetches the current conditions for those coordinates, and the app prints a short report. You run it like python weather.py London --units imperial and get the temperature, humidity, wind speed, and a plain-English condition back.
1. Code Example
1.1 Install the dependency
The app needs one third-party package, requests, for making HTTP calls. Everything else comes from the Python standard library.
pip install requests
requests is the only install; argparse (command-line arguments) and sys (exit codes and stderr) ship with Python itself, so there is no virtual-environment setup beyond this one package.
1.2 The weather app
Save the following as weather.py. It is split into small functions so each step (find the city, fetch the weather, print the report) can be read and tested on its own.
#!/usr/bin/env python3
"""Command-line weather app using the free Open-Meteo API (no API key needed)."""
import argparse
import sys
import requests
GEOCODE_URL = "https://geocoding-api.open-meteo.com/v1/search"
FORECAST_URL = "https://api.open-meteo.com/v1/forecast"
WEATHER_CODES = {
0: "Clear sky",
1: "Mainly clear",
2: "Partly cloudy",
3: "Overcast",
45: "Fog",
48: "Rime fog",
51: "Light drizzle",
53: "Moderate drizzle",
55: "Dense drizzle",
61: "Slight rain",
63: "Moderate rain",
65: "Heavy rain",
71: "Slight snow",
73: "Moderate snow",
75: "Heavy snow",
80: "Rain showers",
95: "Thunderstorm",
}
def find_location(city):
"""Turn a city name into (name, country, latitude, longitude)."""
response = requests.get(GEOCODE_URL, params={"name": city, "count": 1}, timeout=10)
response.raise_for_status()
results = response.json().get("results")
if not results:
raise ValueError(f"City not found: {city}")
place = results[0]
return place["name"], place.get("country", ""), place["latitude"], place["longitude"]
def fetch_weather(latitude, longitude, units):
"""Fetch current conditions for a coordinate pair."""
imperial = units == "imperial"
params = {
"latitude": latitude,
"longitude": longitude,
"current": "temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code",
"temperature_unit": "fahrenheit" if imperial else "celsius",
"wind_speed_unit": "mph" if imperial else "kmh",
}
response = requests.get(FORECAST_URL, params=params, timeout=10)
response.raise_for_status()
data = response.json()
return data["current"], data["current_units"]
def print_report(name, country, current, units):
"""Print a formatted weather report."""
condition = WEATHER_CODES.get(current["weather_code"], "Unknown")
print(f"Weather in {name}, {country}")
print("-" * 30)
print(f"Condition : {condition}")
print(f"Temperature: {current['temperature_2m']}{units['temperature_2m']}")
print(f"Humidity : {current['relative_humidity_2m']}{units['relative_humidity_2m']}")
print(f"Wind speed : {current['wind_speed_10m']} {units['wind_speed_10m']}")
def main():
parser = argparse.ArgumentParser(description="Show current weather for a city.")
parser.add_argument("city", help="City name, e.g. London")
parser.add_argument("--units", choices=["metric", "imperial"], default="metric",
help="metric (C, km/h) or imperial (F, mph)")
args = parser.parse_args()
try:
name, country, lat, lon = find_location(args.city)
current, units = fetch_weather(lat, lon, args.units)
except ValueError as error:
print(error, file=sys.stderr)
sys.exit(1)
except requests.RequestException as error:
print(f"Network error: {error}", file=sys.stderr)
sys.exit(2)
print_report(name, country, current, units)
if __name__ == "__main__":
main()
The two URL constants and the WEATHER_CODES dictionary sit at the top because they are the only things that change if the API or wording changes; the dictionary maps the numeric weather codes the API returns to readable text, and falls back to “Unknown” for any code it does not list. find_location() calls the geocoding endpoint with count=1 to get only the best match, and raises a ValueError when the API returns no results, so a typo in the city name becomes a clear message instead of a crash on a missing key. fetch_weather() asks the forecast endpoint for just the four fields the report uses, converts the unit choice into the API’s temperature_unit and wind_speed_unit parameters, and returns both the values and the matching unit labels so the report never has to guess whether it is showing °C or °F. Both HTTP calls set timeout=10 so a hung connection cannot freeze the app, and raise_for_status() turns 4xx/5xx responses into exceptions. print_report() only formats output and does no network work. main() uses argparse to read the city and the optional --units flag (restricted to metric or imperial and defaulting to metric), then separates failures: a ValueError (city not found) exits with code 1, while a requests.RequestException (no network, timeout, bad response) exits with code 2, and both messages go to stderr so they never mix with normal output. The if __name__ == "__main__" guard lets other scripts import these functions without running the app.
1.3 Run the app
Pass a city name as the first argument. Quote names that contain spaces, and add --units imperial for Fahrenheit and mph.
python weather.py London python weather.py "New York" --units imperial python weather.py Xyzzyqwerty
The first command uses the default metric units, the second shows how a multi-word city and the units flag combine, and the third deliberately uses a city that does not exist to trigger the error path.
1.4 Output
Running the first and last commands produces the following. The weather values come from live data, so yours will differ depending on the current conditions.
$ python weather.py London Weather in London, United Kingdom ------------------------------ Condition : Overcast Temperature: 14.2°C Humidity : 76% Wind speed : 11.5 km/h $ python weather.py Xyzzyqwerty City not found: Xyzzyqwerty
The report lines are aligned by the fixed-width labels in print_report(), and the unit symbols (°C, %, km/h) are not hardcoded: they come straight from the API’s current_units block, which is why switching to --units imperial changes them to °F and mph with no extra code. The second block shows the error path: the geocoding call returns no results, so find_location() raises the ValueError, main() prints its message to stderr, and the process exits with code 1 (check it with echo $?).
2. Conclusion
This app shows the full shape of a small command-line tool: read input with argparse, call an API with timeouts and status checks, turn failures into clear messages with distinct exit codes, and keep fetching, parsing, and printing in separate functions. From here you can extend it without restructuring: add a --days option that reads the daily forecast fields, cache the geocoding result so repeat lookups skip a network call, or print the report as JSON for use in other scripts. The same pattern of one function per step carries over to most small API-backed tools you will build next.


