Weather App Using Tkinter & OpenWeatherMap API | CBSE Class 12 Computer Science Investigatory Project

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()

 

Copywrite © 2020-2026, CBSE Python,
All Rights Reserved