Weather App Using Tkinter
# ============================================================
# WEATHER APP — TKINTER + OPENWEATHERMAP API
# CBSE Class 12 Computer Science Investigatory Project
# Topics Covered: Tkinter GUI, requests library, JSON parsing,
# API integration, File Handling (search history)
# ============================================================
#
# REQUIREMENTS:
# pip install requests
# pip install pillow (for icons/images - optional)
#
# API SETUP (FREE):
# 1. Go to https://openweathermap.org/api
# 2. Sign up for a free account
# 3. Copy your API key and paste it below (API_KEY = "...")
# 4. Free tier allows 60 calls/minute — more than enough
#
# HOW TO RUN:
# python weather_app.py
# ============================================================
import tkinter as tk
from tkinter import ttk, messagebox, font as tkfont
import requests
import json
import os
from datetime import datetime
# ─────────────────────────────────────────────
# CONFIGURATION
# ─────────────────────────────────────────────
API_KEY = "YOUR_API_KEY_HERE" # ← Paste your OpenWeatherMap API key here
BASE_URL = "https://api.openweathermap.org/data/2.5/weather"
FORECAST_URL = "https://api.openweathermap.org/data/2.5/forecast"
HISTORY_FILE = "search_history.txt"
# Weather condition → emoji mapping
WEATHER_ICONS = {
"Clear" : "☀️",
"Clouds" : "☁️",
"Rain" : "🌧️",
"Drizzle" : "🌦️",
"Thunderstorm" : "⛈️",
"Snow" : "❄️",
"Mist" : "🌫️",
"Fog" : "🌫️",
"Haze" : "🌫️",
"Smoke" : "🌫️",
"Dust" : "🌪️",
"Tornado" : "🌪️",
}
# Color themes for each weather condition
THEMES = {
"Clear" : {"bg": "#1a1a2e", "card": "#16213e", "accent": "#f9ca24", "text": "#ffffff", "sub": "#a0a0b0"},
"Clouds" : {"bg": "#2c3e50", "card": "#34495e", "accent": "#bdc3c7", "text": "#ecf0f1", "sub": "#95a5a6"},
"Rain" : {"bg": "#1e3799", "card": "#0c2461", "accent": "#74b9ff", "text": "#dfe6e9", "sub": "#a29bfe"},
"Drizzle" : {"bg": "#2d3561", "card": "#1e2a5e", "accent": "#74b9ff", "text": "#dfe6e9", "sub": "#a29bfe"},
"Thunderstorm" : {"bg": "#1a1a2e", "card": "#16213e", "accent": "#fdcb6e", "text": "#dfe6e9", "sub": "#6c5ce7"},
"Snow" : {"bg": "#dfe6e9", "card": "#b2bec3", "accent": "#0984e3", "text": "#2d3436", "sub": "#636e72"},
"default" : {"bg": "#0f0c29", "card": "#302b63", "accent": "#e94560", "text": "#ffffff", "sub": "#a0a0b0"},
}
# ─────────────────────────────────────────────
# FILE HANDLING — SEARCH HISTORY
# ─────────────────────────────────────────────
def save_to_history(city):
"""Save searched city to history file."""
history = load_history()
if city not in history:
history.insert(0, city)
history = history[:10] # Keep last 10 cities
with open(HISTORY_FILE, "w") as f:
f.write("\n".join(history))
def load_history():
"""Load search history from file."""
if not os.path.exists(HISTORY_FILE):
return []
with open(HISTORY_FILE, "r") as f:
return [line.strip() for line in f.readlines() if line.strip()]
# ─────────────────────────────────────────────
# API CALLS
# ─────────────────────────────────────────────
def fetch_weather(city):
"""Fetch current weather data from OpenWeatherMap."""
if API_KEY == "YOUR_API_KEY_HERE":
# Demo mode with fake data for testing without API key
return demo_weather_data(city)
try:
params = {"q": city, "appid": API_KEY, "units": "metric"}
response = requests.get(BASE_URL, params=params, timeout=10)
if response.status_code == 200:
return response.json(), None
elif response.status_code == 404:
return None, "City not found. Please check the spelling."
elif response.status_code == 401:
return None, "Invalid API key. Please check your key."
else:
return None, f"Error {response.status_code}: Could not fetch data."
except requests.exceptions.ConnectionError:
return None, "No internet connection. Please check your network."
except requests.exceptions.Timeout:
return None, "Request timed out. Please try again."
except Exception as e:
return None, f"Unexpected error: {str(e)}"
def fetch_forecast(city):
"""Fetch 5-day / 3-hour forecast."""
if API_KEY == "YOUR_API_KEY_HERE":
return demo_forecast_data()
try:
params = {"q": city, "appid": API_KEY, "units": "metric", "cnt": 40}
response = requests.get(FORECAST_URL, params=params, timeout=10)
if response.status_code == 200:
return response.json(), None
return None, "Could not fetch forecast."
except Exception as e:
return None, str(e)
def demo_weather_data(city):
"""Demo data when no API key is set — for testing UI."""
return {
"name": city or "Delhi",
"sys": {"country": "IN", "sunrise": 1700000000, "sunset": 1700040000},
"main": {
"temp": 28.4, "feels_like": 31.2,
"temp_min": 24.0, "temp_max": 32.5,
"humidity": 65, "pressure": 1012
},
"weather": [{"main": "Clear", "description": "clear sky", "icon": "01d"}],
"wind": {"speed": 3.5, "deg": 180},
"visibility": 10000,
"clouds": {"all": 10},
"coord": {"lat": 28.6, "lon": 77.2},
}, None
def demo_forecast_data():
import random
items = []
conditions = ["Clear","Clouds","Rain","Clear","Clouds"]
descs = ["clear sky","few clouds","light rain","clear sky","overcast clouds"]
for i in range(5):
items.append({
"dt": 1700000000 + i * 86400,
"main": {
"temp": round(28 + random.uniform(-5,5),1),
"humidity": random.randint(50,90),
},
"weather": [{"main": conditions[i], "description": descs[i]}],
"wind": {"speed": round(random.uniform(1,8),1)},
})
return {"list": items}, None
# ─────────────────────────────────────────────
# MAIN APPLICATION CLASS
# ─────────────────────────────────────────────
class WeatherApp:
def __init__(self, root):
self.root = root
self.root.title("Weather App — CBSE Python Project")
self.root.geometry("520x780")
self.root.resizable(False, False)
self.root.configure(bg="#0f0c29")
self.current_theme = THEMES["default"]
self.current_city = ""
self.unit_var = tk.StringVar(value="metric") # metric / imperial
self._build_ui()
self._load_history_dropdown()
# ── UI CONSTRUCTION ──────────────────────
def _build_ui(self):
theme = self.current_theme
# ── Top Bar ──
top = tk.Frame(self.root, bg=theme["bg"], pady=8)
top.pack(fill="x")
tk.Label(top, text="⛅ WEATHER APP", bg=theme["bg"],
fg=theme["accent"], font=("Helvetica", 13, "bold")).pack(side="left", padx=16)
# Unit toggle
unit_frame = tk.Frame(top, bg=theme["bg"])
unit_frame.pack(side="right", padx=12)
tk.Radiobutton(unit_frame, text="°C", variable=self.unit_var, value="metric",
bg=theme["bg"], fg=theme["sub"], selectcolor=theme["card"],
activebackground=theme["bg"], font=("Helvetica",10),
command=self._refresh).pack(side="left")
tk.Radiobutton(unit_frame, text="°F", variable=self.unit_var, value="imperial",
bg=theme["bg"], fg=theme["sub"], selectcolor=theme["card"],
activebackground=theme["bg"], font=("Helvetica",10),
command=self._refresh).pack(side="left")
# ── Search Bar ──
search_frame = tk.Frame(self.root, bg=theme["bg"], pady=4)
search_frame.pack(fill="x", padx=16)
self.search_var = tk.StringVar()
self.search_entry = tk.Entry(
search_frame, textvariable=self.search_var,
font=("Helvetica", 13), bg=theme["card"], fg=theme["text"],
insertbackground=theme["text"], relief="flat",
highlightthickness=1, highlightcolor=theme["accent"],
highlightbackground=theme["sub"]
)
self.search_entry.pack(side="left", fill="x", expand=True, ipady=8, padx=(0,8))
self.search_entry.bind("<Return>", lambda e: self._search())
self.search_entry.insert(0, "Enter city name...")
self.search_entry.bind("<FocusIn>", self._clear_placeholder)
self.search_entry.bind("<FocusOut>", self._restore_placeholder)
search_btn = tk.Button(
search_frame, text="Search", command=self._search,
bg=theme["accent"], fg=theme["bg"] if theme["bg"] != "#dfe6e9" else "#ffffff",
font=("Helvetica", 11, "bold"), relief="flat",
padx=14, pady=8, cursor="hand2",
activebackground=theme["sub"]
)
search_btn.pack(side="right")
# ── History Dropdown ──
hist_frame = tk.Frame(self.root, bg=theme["bg"])
hist_frame.pack(fill="x", padx=16, pady=(0,6))
tk.Label(hist_frame, text="Recent:", bg=theme["bg"],
fg=theme["sub"], font=("Helvetica",9)).pack(side="left")
self.history_combo = ttk.Combobox(hist_frame, width=25, state="readonly",
font=("Helvetica",9))
self.history_combo.pack(side="left", padx=6)
self.history_combo.bind("<<ComboboxSelected>>", self._from_history)
tk.Button(hist_frame, text="Clear History", command=self._clear_history,
bg=theme["card"], fg=theme["sub"], font=("Helvetica",8),
relief="flat", cursor="hand2", padx=6, pady=2).pack(side="right")
# ── Main Weather Card ──
self.card = tk.Frame(self.root, bg=theme["card"],
padx=20, pady=20, relief="flat")
self.card.pack(fill="x", padx=16, pady=6)
self.icon_label = tk.Label(self.card, text="🌤️", bg=theme["card"],
font=("Helvetica", 52))
self.icon_label.pack()
self.city_label = tk.Label(self.card, text="— Search a city —",
bg=theme["card"], fg=theme["text"],
font=("Helvetica", 20, "bold"))
self.city_label.pack()
self.country_label = tk.Label(self.card, text="",
bg=theme["card"], fg=theme["sub"],
font=("Helvetica", 11))
self.country_label.pack()
self.temp_label = tk.Label(self.card, text="",
bg=theme["card"], fg=theme["accent"],
font=("Helvetica", 58, "bold"))
self.temp_label.pack()
self.desc_label = tk.Label(self.card, text="",
bg=theme["card"], fg=theme["sub"],
font=("Helvetica", 13, "italic"))
self.desc_label.pack(pady=(0,4))
self.feels_label = tk.Label(self.card, text="",
bg=theme["card"], fg=theme["sub"],
font=("Helvetica", 10))
self.feels_label.pack()
# ── Stats Grid ──
self.stats_frame = tk.Frame(self.root, bg=theme["bg"])
self.stats_frame.pack(fill="x", padx=16, pady=4)
self.stat_widgets = {}
stats = [
("💧 Humidity", "humidity", "0%"),
("💨 Wind", "wind", "0 km/h"),
("👁 Visibility", "visibility", "0 km"),
("🌡 Pressure", "pressure", "0 hPa"),
("🌅 Sunrise", "sunrise", "--:--"),
("🌇 Sunset", "sunset", "--:--"),
]
for i, (title, key, default) in enumerate(stats):
col = i % 3
row = i // 3
f = tk.Frame(self.stats_frame, bg=theme["card"],
padx=10, pady=10)
f.grid(row=row, column=col, padx=4, pady=4, sticky="ew")
self.stats_frame.columnconfigure(col, weight=1)
tk.Label(f, text=title, bg=theme["card"],
fg=theme["sub"], font=("Helvetica", 9)).pack()
val_lbl = tk.Label(f, text=default, bg=theme["card"],
fg=theme["text"], font=("Helvetica", 12, "bold"))
val_lbl.pack()
self.stat_widgets[key] = val_lbl
# ── Min / Max Bar ──
self.minmax_frame = tk.Frame(self.root, bg=theme["card"],
padx=16, pady=10)
self.minmax_frame.pack(fill="x", padx=16, pady=4)
self.min_label = tk.Label(self.minmax_frame, text="↓ Min: --",
bg=theme["card"], fg="#74b9ff",
font=("Helvetica", 11, "bold"))
self.min_label.pack(side="left", expand=True)
self.max_label = tk.Label(self.minmax_frame, text="↑ Max: --",
bg=theme["card"], fg="#e17055",
font=("Helvetica", 11, "bold"))
self.max_label.pack(side="right", expand=True)
# ── 5-Day Forecast ──
tk.Label(self.root, text="5 - DAY FORECAST",
bg=theme["bg"], fg=theme["sub"],
font=("Helvetica", 9, "bold")).pack(anchor="w", padx=20, pady=(6,0))
self.forecast_frame = tk.Frame(self.root, bg=theme["bg"])
self.forecast_frame.pack(fill="x", padx=16, pady=4)
self.forecast_cols = []
for i in range(5):
col_frame = tk.Frame(self.forecast_frame, bg=theme["card"],
padx=8, pady=8)
col_frame.grid(row=0, column=i, padx=3, sticky="nsew")
self.forecast_frame.columnconfigure(i, weight=1)
day_lbl = tk.Label(col_frame, text="---", bg=theme["card"],
fg=theme["sub"], font=("Helvetica",8))
day_lbl.pack()
icon_lbl = tk.Label(col_frame, text="🌤", bg=theme["card"],
font=("Helvetica",18))
icon_lbl.pack()
temp_lbl = tk.Label(col_frame, text="--°", bg=theme["card"],
fg=theme["text"], font=("Helvetica",10,"bold"))
temp_lbl.pack()
desc_lbl = tk.Label(col_frame, text="---", bg=theme["card"],
fg=theme["sub"], font=("Helvetica",7),
wraplength=60)
desc_lbl.pack()
self.forecast_cols.append((day_lbl, icon_lbl, temp_lbl, desc_lbl))
# ── Status Bar ──
self.status_var = tk.StringVar(value="Enter a city name and press Search.")
tk.Label(self.root, textvariable=self.status_var, bg=theme["bg"],
fg=theme["sub"], font=("Helvetica", 9),
anchor="center").pack(fill="x", pady=6)
# ── SEARCH LOGIC ─────────────────────────
def _clear_placeholder(self, event):
if self.search_entry.get() == "Enter city name...":
self.search_entry.delete(0, tk.END)
def _restore_placeholder(self, event):
if not self.search_entry.get():
self.search_entry.insert(0, "Enter city name...")
def _search(self):
city = self.search_var.get().strip()
if not city or city == "Enter city name...":
messagebox.showwarning("Input Error", "Please enter a city name.")
return
self.status_var.set(f"Fetching weather for '{city}'...")
self.root.update()
self._fetch_and_display(city)
def _refresh(self):
if self.current_city:
self._fetch_and_display(self.current_city)
def _from_history(self, event):
city = self.history_combo.get()
if city:
self.search_var.set(city)
self._fetch_and_display(city)
def _clear_history(self):
if os.path.exists(HISTORY_FILE):
os.remove(HISTORY_FILE)
self.history_combo["values"] = []
self.status_var.set("Search history cleared.")
def _load_history_dropdown(self):
history = load_history()
self.history_combo["values"] = history
# ── DATA → UI UPDATE ─────────────────────
def _fetch_and_display(self, city):
# Current weather
data, err = fetch_weather(city)
if err:
messagebox.showerror("Error", err)
self.status_var.set(f"Error: {err}")
return
self.current_city = city
save_to_history(city)
self._load_history_dropdown()
# Apply theme based on weather condition
condition = data["weather"][0]["main"]
theme = THEMES.get(condition, THEMES["default"])
self.current_theme = theme
self._apply_theme(theme)
unit = self.unit_var.get()
symbol = "°C" if unit == "metric" else "°F"
speed = "m/s" if unit == "metric" else "mph"
# ── Main card ──
icon = WEATHER_ICONS.get(condition, "🌤️")
self.icon_label.config(text=icon)
self.city_label.config(text=data["name"])
self.country_label.config(
text=f"{data['sys']['country']} • {datetime.now().strftime('%d %b %Y %H:%M')}"
)
temp = data["main"]["temp"]
self.temp_label.config(text=f"{temp:.0f}{symbol}")
self.desc_label.config(text=data["weather"][0]["description"].title())
feels = data["main"]["feels_like"]
self.feels_label.config(text=f"Feels like {feels:.0f}{symbol}")
# ── Stats ──
sunrise = datetime.fromtimestamp(data["sys"]["sunrise"]).strftime("%H:%M")
sunset = datetime.fromtimestamp(data["sys"]["sunset"]).strftime("%H:%M")
vis_km = data.get("visibility", 0) / 1000
self.stat_widgets["humidity"].config(
text=f"{data['main']['humidity']}%")
self.stat_widgets["wind"].config(
text=f"{data['wind']['speed']} {speed}")
self.stat_widgets["visibility"].config(
text=f"{vis_km:.1f} km")
self.stat_widgets["pressure"].config(
text=f"{data['main']['pressure']} hPa")
self.stat_widgets["sunrise"].config(text=sunrise)
self.stat_widgets["sunset"].config(text=sunset)
# ── Min / Max ──
self.min_label.config(
text=f"↓ Min: {data['main']['temp_min']:.0f}{symbol}")
self.max_label.config(
text=f"↑ Max: {data['main']['temp_max']:.0f}{symbol}")
# ── Forecast ──
forecast_data, ferr = fetch_forecast(city)
if not ferr and forecast_data:
# Pick one reading per day (noon slot)
daily = {}
for item in forecast_data["list"]:
day = datetime.fromtimestamp(item["dt"]).strftime("%A")
if day not in daily:
daily[day] = item
if len(daily) == 5:
break
for i, (day, item) in enumerate(list(daily.items())[:5]):
cond = item["weather"][0]["main"]
d_icon = WEATHER_ICONS.get(cond, "🌤")
d_temp = item["main"]["temp"]
d_desc = item["weather"][0]["description"].title()
day_short = day[:3]
self.forecast_cols[i][0].config(text=day_short)
self.forecast_cols[i][1].config(text=d_icon)
self.forecast_cols[i][2].config(text=f"{d_temp:.0f}{symbol}")
self.forecast_cols[i][3].config(text=d_desc)
self.status_var.set(
f"✓ Last updated: {datetime.now().strftime('%H:%M:%S')} • "
f"Coordinates: {data['coord']['lat']}, {data['coord']['lon']}"
)
def _apply_theme(self, theme):
"""Repaint all widgets with the new weather theme."""
self.root.configure(bg=theme["bg"])
for widget in self.root.winfo_children():
self._repaint(widget, theme)
def _repaint(self, widget, theme):
wtype = widget.winfo_class()
try:
if wtype in ("Frame", "Label"):
orig_bg = widget.cget("bg")
# Map old theme colors to new
if orig_bg == self.current_theme.get("bg", ""):
widget.config(bg=theme["bg"])
elif orig_bg == self.current_theme.get("card", ""):
widget.config(bg=theme["card"])
if wtype == "Label":
orig_fg = widget.cget("fg")
if orig_fg == self.current_theme.get("text",""):
widget.config(fg=theme["text"])
elif orig_fg == self.current_theme.get("sub",""):
widget.config(fg=theme["sub"])
elif orig_fg == self.current_theme.get("accent",""):
widget.config(fg=theme["accent"])
except Exception:
pass
for child in widget.winfo_children():
self._repaint(child, theme)
# ─────────────────────────────────────────────
# ENTRY POINT
# ─────────────────────────────────────────────
if __name__ == "__main__":
root = tk.Tk()
# Centre window on screen
root.update_idletasks()
w, h = 520, 780
x = (root.winfo_screenwidth() - w) // 2
y = (root.winfo_screenheight() - h) // 2
root.geometry(f"{w}x{h}+{x}+{y}")
app = WeatherApp(root)
root.mainloop()