Simple Weather Forecast App
Abstract
Consuming a real web API is a rite of passage, and weather is the perfect first API: free, well-documented, and instantly gratifying. In this tutorial you build a Tkinter app that queries the OpenWeatherMap API for a city’s current conditions and displays temperature, “feels like”, humidity, and a description. Then you grow it into something polished — a 5-day forecast, weather icons, °C/°F toggle, geolocation, response caching, and the kind of error handling that separates a demo from a tool.
You will leave understanding:
- How to register for and use an API key safely.
- The request → JSON → parse → display pipeline behind every API client.
- How to read nested JSON (
data["weather"][0]["description"]data["weather"][0]["description"]) without crashing. - Why you check
status_codestatus_codeand handle the unhappy paths first.
Prerequisites
- Python 3.6 or above.
- A text editor or IDE.
- The
requestsrequestslibrary:pip install requestspip install requests. - A free OpenWeatherMap API key from openweathermap.org/api.
- Tkinter (bundled with Python).
Getting Started
Get an API key
- Sign up at openweathermap.org (free tier is plenty).
- Copy your API key from the dashboard.
- New keys can take a few minutes to activate — don’t panic if the first call 401s.
Create the project
- Create a folder named
weather-appweather-app. - Inside it, create
simple_weather_forecast_app.pysimple_weather_forecast_app.py. - Install the dependency:
pip install requestspip install requests.
Write the code
simple_weather_forecast_app.py
Source"""
Simple Weather Forecast App
A Python application that fetches and displays weather forecast data. Features include:
- Fetching weather data from an API.
- Displaying the forecast in a user-friendly format.
"""
import requests
from tkinter import Tk, Label, Entry, Button, messagebox
API_KEY = "your_openweathermap_api_key"
BASE_URL = "http://api.openweathermap.org/data/2.5/weather"
class WeatherForecastApp:
def __init__(self, root):
self.root = root
self.root.title("Simple Weather Forecast App")
Label(root, text="Enter City Name:").grid(row=0, column=0, padx=10, pady=10)
self.city_entry = Entry(root, width=30)
self.city_entry.grid(row=0, column=1, padx=10, pady=10)
Button(root, text="Get Weather", command=self.get_weather).grid(row=1, column=0, columnspan=2, pady=10)
self.result_label = Label(root, text="", wraplength=400, justify="left")
self.result_label.grid(row=2, column=0, columnspan=2, padx=10, pady=10)
def get_weather(self):
"""Fetch weather data for the entered city."""
city = self.city_entry.get()
if not city:
messagebox.showerror("Error", "Please enter a city name.")
return
params = {"q": city, "appid": API_KEY, "units": "metric"}
try:
response = requests.get(BASE_URL, params=params)
data = response.json()
if response.status_code == 200:
weather = data["weather"][0]["description"].capitalize()
temp = data["main"]["temp"]
feels_like = data["main"]["feels_like"]
humidity = data["main"]["humidity"]
result = (
f"Weather in {city}:\n"
f"Condition: {weather}\n"
f"Temperature: {temp}°C\n"
f"Feels Like: {feels_like}°C\n"
f"Humidity: {humidity}%"
)
self.result_label.config(text=result)
else:
messagebox.showerror("Error", data.get("message", "Failed to fetch weather data."))
except Exception as e:
messagebox.showerror("Error", f"An error occurred: {e}")
def main():
root = Tk()
app = WeatherForecastApp(root)
root.mainloop()
if __name__ == "__main__":
main()
"""
Simple Weather Forecast App
A Python application that fetches and displays weather forecast data. Features include:
- Fetching weather data from an API.
- Displaying the forecast in a user-friendly format.
"""
import requests
from tkinter import Tk, Label, Entry, Button, messagebox
API_KEY = "your_openweathermap_api_key"
BASE_URL = "http://api.openweathermap.org/data/2.5/weather"
class WeatherForecastApp:
def __init__(self, root):
self.root = root
self.root.title("Simple Weather Forecast App")
Label(root, text="Enter City Name:").grid(row=0, column=0, padx=10, pady=10)
self.city_entry = Entry(root, width=30)
self.city_entry.grid(row=0, column=1, padx=10, pady=10)
Button(root, text="Get Weather", command=self.get_weather).grid(row=1, column=0, columnspan=2, pady=10)
self.result_label = Label(root, text="", wraplength=400, justify="left")
self.result_label.grid(row=2, column=0, columnspan=2, padx=10, pady=10)
def get_weather(self):
"""Fetch weather data for the entered city."""
city = self.city_entry.get()
if not city:
messagebox.showerror("Error", "Please enter a city name.")
return
params = {"q": city, "appid": API_KEY, "units": "metric"}
try:
response = requests.get(BASE_URL, params=params)
data = response.json()
if response.status_code == 200:
weather = data["weather"][0]["description"].capitalize()
temp = data["main"]["temp"]
feels_like = data["main"]["feels_like"]
humidity = data["main"]["humidity"]
result = (
f"Weather in {city}:\n"
f"Condition: {weather}\n"
f"Temperature: {temp}°C\n"
f"Feels Like: {feels_like}°C\n"
f"Humidity: {humidity}%"
)
self.result_label.config(text=result)
else:
messagebox.showerror("Error", data.get("message", "Failed to fetch weather data."))
except Exception as e:
messagebox.showerror("Error", f"An error occurred: {e}")
def main():
root = Tk()
app = WeatherForecastApp(root)
root.mainloop()
if __name__ == "__main__":
main()
Run it
C:\Users\Your Name\weather-app> python simple_weather_forecast_app.py
# Enter a city name, click "Get Weather".
# Make sure you replaced API_KEY with your real key first.C:\Users\Your Name\weather-app> python simple_weather_forecast_app.py
# Enter a city name, click "Get Weather".
# Make sure you replaced API_KEY with your real key first.Step-by-Step Explanation
1. The endpoint and key
API_KEY = "your_openweathermap_api_key"
BASE_URL = "http://api.openweathermap.org/data/2.5/weather"API_KEY = "your_openweathermap_api_key"
BASE_URL = "http://api.openweathermap.org/data/2.5/weather"BASE_URLBASE_URL is the “current weather” endpoint. The key authenticates you and counts against your rate limit.
2. Building the request
params = {"q": city, "appid": API_KEY, "units": "metric"}
response = requests.get(BASE_URL, params=params)
data = response.json()params = {"q": city, "appid": API_KEY, "units": "metric"}
response = requests.get(BASE_URL, params=params)
data = response.json()Passing a paramsparams dict lets requestsrequests build the query string for you (?q=London&appid=...&units=metric?q=London&appid=...&units=metric) and handles URL-encoding spaces and special characters. units="metric"units="metric" gives Celsius; "imperial""imperial" gives Fahrenheit.
3. Check status before parsing
if response.status_code == 200:
weather = data["weather"][0]["description"].capitalize()
temp = data["main"]["temp"]
...
else:
messagebox.showerror("Error", data.get("message", "Failed to fetch weather data."))if response.status_code == 200:
weather = data["weather"][0]["description"].capitalize()
temp = data["main"]["temp"]
...
else:
messagebox.showerror("Error", data.get("message", "Failed to fetch weather data."))This is the most important habit in API work: handle the failure path first. A typo’d city returns 404404 with a helpful messagemessage field — surface it instead of crashing on a missing key.
4. Reading nested JSON
data["weather"][0]["description"] # list → first item → field
data["main"]["temp"] # nested dictdata["weather"][0]["description"] # list → first item → field
data["main"]["temp"] # nested dictOpenWeatherMap nests weatherweather inside a list (a place can have several conditions) and groups numbers under mainmain. Knowing the shape of the JSON is half the battle — print datadata once and study it.
Protect Your API Key
Never hard-code secrets. Use an environment variable:
import os
API_KEY = os.environ["OWM_API_KEY"] # set OWM_API_KEY in your shell, not in codeimport os
API_KEY = os.environ["OWM_API_KEY"] # set OWM_API_KEY in your shell, not in code# PowerShell
$env:OWM_API_KEY = "your_key_here"# PowerShell
$env:OWM_API_KEY = "your_key_here"This keeps the key out of screenshots, git history, and shared code.
Add a 5-Day Forecast
The forecast endpoint returns data in 3-hour steps:
FORECAST_URL = "http://api.openweathermap.org/data/2.5/forecast"
def five_day(city):
r = requests.get(FORECAST_URL,
params={"q": city, "appid": API_KEY, "units": "metric"},
timeout=10)
r.raise_for_status()
# Pick the midday (12:00) reading for each day
return [item for item in r.json()["list"] if "12:00:00" in item["dt_txt"]]FORECAST_URL = "http://api.openweathermap.org/data/2.5/forecast"
def five_day(city):
r = requests.get(FORECAST_URL,
params={"q": city, "appid": API_KEY, "units": "metric"},
timeout=10)
r.raise_for_status()
# Pick the midday (12:00) reading for each day
return [item for item in r.json()["list"] if "12:00:00" in item["dt_txt"]]Loop the result into five labels or a small table.
Show Weather Icons
Each condition carries an icon code:
from tkinter import PhotoImage
import requests, io
icon_code = data["weather"][0]["icon"] # e.g. "10d"
url = f"http://openweathermap.org/img/wn/{icon_code}@2x.png"
img_bytes = requests.get(url, timeout=10).content
# Save then load, or use Pillow's ImageTk for in-memory displayfrom tkinter import PhotoImage
import requests, io
icon_code = data["weather"][0]["icon"] # e.g. "10d"
url = f"http://openweathermap.org/img/wn/{icon_code}@2x.png"
img_bytes = requests.get(url, timeout=10).content
# Save then load, or use Pillow's ImageTk for in-memory displayAdd a °C / °F Toggle
Store the raw value once, convert for display:
def to_fahrenheit(celsius):
return celsius * 9 / 5 + 32
label.config(text=f"{temp}°C" if self.metric else f"{to_fahrenheit(temp):.1f}°F")def to_fahrenheit(celsius):
return celsius * 9 / 5 + 32
label.config(text=f"{temp}°C" if self.metric else f"{to_fahrenheit(temp):.1f}°F")Common Mistakes
| Problem | Cause | Fix |
|---|---|---|
401 Unauthorized401 Unauthorized | Key missing, wrong, or not yet active | Check the key; wait a few minutes after signup |
KeyError: 'main'KeyError: 'main' | Parsed JSON before checking status | Guard with if status_code == 200if status_code == 200 first |
| App freezes on slow network | No timeouttimeout | Add timeout=10timeout=10; fetch on a background thread |
| Spaces in city name break URL | Manual string concatenation | Use the params=params= dict — it encodes for you |
| Wrong temperature scale | Forgot unitsunits | Set units="metric"units="metric" or "imperial""imperial" |
| Rate-limit errors | Polling too often | Cache responses for a few minutes |
Variations to Try
- Geolocation — detect the user’s city via their IP and load it on launch.
- Hourly chart — plot the next 24 hours with
matplotlibmatplotlib. - Favorites — save a list of cities and refresh them all at once.
- Severe-weather alerts — use the One Call API’s
alertsalertsfield. - Background by condition — change the window color for sunny/rainy/snow.
- Caching layer — store responses with a timestamp; skip the API within 10 minutes.
- Voice output — read the forecast aloud (see Weather App with Voice Commands).
Real-World Applications
- Travel apps — destination weather at a glance.
- Agriculture & logistics — planning around conditions.
- Dashboards — a weather widget on a home-automation panel.
- Event planning — outdoor-event go/no-go decisions.
Educational Value
- REST APIs — keys, query parameters, JSON responses.
- Defensive parsing — status checks and
.get().get()with defaults. - Secret management — environment variables over hard-coding.
- Caching & rate limits — being a good API citizen.
Next Steps
- Move the key into an environment variable.
- Add a 5-day forecast and weather icons.
- Implement a °C/°F toggle and response caching.
- Detect the user’s city with geolocation.
Conclusion
You built a weather app that turns a city name into live conditions, then layered on a forecast, icons, unit toggles, and caching — all on top of the same request → parse → display loop you’ll reuse for every API you ever touch. The skill transfers directly: swap the endpoint and you have a stock ticker, a currency converter, or a news reader. Full source on GitHub. Explore more API projects on Python Central Hub.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
