Weather App GUI with API Integration
Abstract
Create a comprehensive desktop weather application that integrates with the OpenWeatherMap API to provide real-time weather data, 5-day forecasts, and weather history. This project demonstrates advanced GUI development, API integration, threading for responsive UI, and data persistence with JSON file handling.
Prerequisites
- Python 3.7 or above
- Text Editor or IDE
- Solid understanding of Python syntax and OOP concepts
- Knowledge of Tkinter for GUI development
- Familiarity with API requests and JSON handling
- Understanding of threading and asynchronous programming
- Basic knowledge of weather data and meteorology concepts
Getting Started
Create a new project
- Create a new project folder and name it
weatherAppGUI
weatherAppGUI
. - Create a new file and name it
weatherappgui.py
weatherappgui.py
. - Sign up for a free API key at OpenWeatherMap.
- Install required dependencies:
pip install requests
pip install requests
- Open the project folder in your favorite text editor or IDE.
- Copy the code below and paste it into your
weatherappgui.py
weatherappgui.py
file.
Write the code
- Add the following code to your
weatherappgui.py
weatherappgui.py
file.
⚙️ Weather App GUI with API Integration
# Weather App with GUI (Tkinter)
import tkinter as tk
from tkinter import ttk, messagebox
import requests
import json
from datetime import datetime, timedelta
from typing import Dict, Optional, List
import threading
class WeatherApp:
def __init__(self, root):
self.root = root
self.root.title("Weather App")
self.root.geometry("800x600")
self.root.configure(bg='#2c3e50')
# API key (you would get this from OpenWeatherMap)
self.api_key = "your_api_key_here" # Replace with actual API key
self.base_url = "http://api.openweathermap.org/data/2.5"
# Store weather data
self.current_weather = None
self.forecast_data = None
self.setup_ui()
def setup_ui(self):
"""Setup the user interface"""
# Title
title_label = tk.Label(
self.root,
text="Weather App",
font=("Arial", 24, "bold"),
bg='#2c3e50',
fg='#ecf0f1'
)
title_label.pack(pady=20)
# Search frame
search_frame = tk.Frame(self.root, bg='#2c3e50')
search_frame.pack(pady=10)
tk.Label(
search_frame,
text="Enter City:",
font=("Arial", 12),
bg='#2c3e50',
fg='#ecf0f1'
).pack(side=tk.LEFT, padx=5)
self.city_entry = tk.Entry(
search_frame,
font=("Arial", 12),
width=20
)
self.city_entry.pack(side=tk.LEFT, padx=5)
self.city_entry.bind('<Return>', lambda event: self.get_weather())
self.search_button = tk.Button(
search_frame,
text="Get Weather",
command=self.get_weather,
font=("Arial", 10, "bold"),
bg='#3498db',
fg='white',
padx=10
)
self.search_button.pack(side=tk.LEFT, padx=5)
# Current weather frame
self.current_frame = tk.LabelFrame(
self.root,
text="Current Weather",
font=("Arial", 14, "bold"),
bg='#34495e',
fg='#ecf0f1',
padx=20,
pady=15
)
self.current_frame.pack(pady=20, padx=20, fill='x')
# Weather info labels
self.weather_labels = {}
self.create_weather_labels()
# Forecast frame
self.forecast_frame = tk.LabelFrame(
self.root,
text="5-Day Forecast",
font=("Arial", 14, "bold"),
bg='#34495e',
fg='#ecf0f1',
padx=20,
pady=15
)
self.forecast_frame.pack(pady=10, padx=20, fill='both', expand=True)
# Buttons frame
buttons_frame = tk.Frame(self.root, bg='#2c3e50')
buttons_frame.pack(pady=10)
self.refresh_button = tk.Button(
buttons_frame,
text="Refresh",
command=self.refresh_weather,
font=("Arial", 10),
bg='#27ae60',
fg='white',
padx=15
)
self.refresh_button.pack(side=tk.LEFT, padx=5)
self.save_button = tk.Button(
buttons_frame,
text="Save Data",
command=self.save_weather_data,
font=("Arial", 10),
bg='#e67e22',
fg='white',
padx=15
)
self.save_button.pack(side=tk.LEFT, padx=5)
# Status bar
self.status_var = tk.StringVar()
self.status_var.set("Ready")
status_bar = tk.Label(
self.root,
textvariable=self.status_var,
relief=tk.SUNKEN,
anchor=tk.W,
bg='#2c3e50',
fg='#ecf0f1'
)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def create_weather_labels(self):
"""Create labels for weather information"""
# City and country
self.weather_labels['city'] = tk.Label(
self.current_frame,
text="City: -",
font=("Arial", 14, "bold"),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['city'].grid(row=0, column=0, columnspan=2, pady=5, sticky='w')
# Temperature
self.weather_labels['temp'] = tk.Label(
self.current_frame,
text="Temperature: -",
font=("Arial", 24, "bold"),
bg='#34495e',
fg='#e74c3c'
)
self.weather_labels['temp'].grid(row=1, column=0, columnspan=2, pady=10)
# Weather description
self.weather_labels['desc'] = tk.Label(
self.current_frame,
text="Description: -",
font=("Arial", 12),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['desc'].grid(row=2, column=0, columnspan=2, pady=5)
# Feels like
self.weather_labels['feels_like'] = tk.Label(
self.current_frame,
text="Feels like: -",
font=("Arial", 11),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['feels_like'].grid(row=3, column=0, pady=5, sticky='w')
# Humidity
self.weather_labels['humidity'] = tk.Label(
self.current_frame,
text="Humidity: -",
font=("Arial", 11),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['humidity'].grid(row=3, column=1, pady=5, sticky='w')
# Pressure
self.weather_labels['pressure'] = tk.Label(
self.current_frame,
text="Pressure: -",
font=("Arial", 11),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['pressure'].grid(row=4, column=0, pady=5, sticky='w')
# Wind speed
self.weather_labels['wind'] = tk.Label(
self.current_frame,
text="Wind: -",
font=("Arial", 11),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['wind'].grid(row=4, column=1, pady=5, sticky='w')
# Visibility
self.weather_labels['visibility'] = tk.Label(
self.current_frame,
text="Visibility: -",
font=("Arial", 11),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['visibility'].grid(row=5, column=0, pady=5, sticky='w')
# UV Index (if available)
self.weather_labels['uv'] = tk.Label(
self.current_frame,
text="UV Index: -",
font=("Arial", 11),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['uv'].grid(row=5, column=1, pady=5, sticky='w')
def get_weather(self):
"""Get weather data for the specified city"""
city = self.city_entry.get().strip()
if not city:
messagebox.showwarning("Warning", "Please enter a city name")
return
# Show loading status
self.status_var.set("Loading weather data...")
self.search_button.config(state='disabled')
# Use threading to prevent UI freezing
thread = threading.Thread(target=self._fetch_weather_data, args=(city,))
thread.daemon = True
thread.start()
def _fetch_weather_data(self, city: str):
"""Fetch weather data in a separate thread"""
try:
# Get current weather
current_url = f"{self.base_url}/weather?q={city}&appid={self.api_key}&units=metric"
# For demo purposes, we'll use mock data
# In real implementation, you would use: response = requests.get(current_url)
mock_current_data = self.get_mock_current_weather(city)
# Get forecast
# forecast_url = f"{self.base_url}/forecast?q={city}&appid={self.api_key}&units=metric"
mock_forecast_data = self.get_mock_forecast_data(city)
# Update UI in main thread
self.root.after(0, self._update_weather_display, mock_current_data, mock_forecast_data)
except Exception as e:
self.root.after(0, self._show_error, f"Error fetching weather data: {str(e)}")
def get_mock_current_weather(self, city: str) -> Dict:
"""Generate mock current weather data for demo"""
import random
temps = [15, 18, 22, 25, 28, 30, 12, 8, 5, 2]
conditions = ["Clear", "Cloudy", "Rainy", "Sunny", "Partly Cloudy", "Overcast"]
return {
"name": city.title(),
"sys": {"country": "XX"},
"main": {
"temp": random.choice(temps),
"feels_like": random.choice(temps) + random.randint(-3, 3),
"humidity": random.randint(30, 80),
"pressure": random.randint(1000, 1030)
},
"weather": [{
"main": random.choice(conditions),
"description": random.choice(conditions).lower()
}],
"wind": {
"speed": random.randint(5, 25)
},
"visibility": random.randint(5000, 10000),
"dt": int(datetime.now().timestamp())
}
def get_mock_forecast_data(self, city: str) -> Dict:
"""Generate mock forecast data for demo"""
import random
forecast_list = []
for i in range(40): # 5 days * 8 times per day (3-hour intervals)
date = datetime.now() + timedelta(hours=i*3)
temp = random.randint(10, 30)
forecast_list.append({
"dt": int(date.timestamp()),
"dt_txt": date.strftime("%Y-%m-%d %H:%M:%S"),
"main": {
"temp": temp,
"temp_min": temp - 2,
"temp_max": temp + 2
},
"weather": [{
"main": random.choice(["Clear", "Cloudy", "Rain"]),
"description": "demo weather"
}]
})
return {"list": forecast_list}
def _update_weather_display(self, current_data: Dict, forecast_data: Dict):
"""Update the weather display with fetched data"""
try:
self.current_weather = current_data
self.forecast_data = forecast_data
# Update current weather
city_country = f"{current_data['name']}, {current_data['sys']['country']}"
self.weather_labels['city'].config(text=f"City: {city_country}")
temp = round(current_data['main']['temp'])
self.weather_labels['temp'].config(text=f"{temp}°C")
desc = current_data['weather'][0]['description'].title()
self.weather_labels['desc'].config(text=f"Description: {desc}")
feels_like = round(current_data['main']['feels_like'])
self.weather_labels['feels_like'].config(text=f"Feels like: {feels_like}°C")
humidity = current_data['main']['humidity']
self.weather_labels['humidity'].config(text=f"Humidity: {humidity}%")
pressure = current_data['main']['pressure']
self.weather_labels['pressure'].config(text=f"Pressure: {pressure} hPa")
wind_speed = current_data['wind']['speed']
self.weather_labels['wind'].config(text=f"Wind: {wind_speed} m/s")
visibility = current_data.get('visibility', 0) / 1000
self.weather_labels['visibility'].config(text=f"Visibility: {visibility:.1f} km")
# Update forecast
self.update_forecast_display(forecast_data)
# Update status
self.status_var.set(f"Last updated: {datetime.now().strftime('%H:%M:%S')}")
except Exception as e:
self._show_error(f"Error updating display: {str(e)}")
finally:
self.search_button.config(state='normal')
def update_forecast_display(self, forecast_data: Dict):
"""Update the forecast display"""
# Clear existing forecast widgets
for widget in self.forecast_frame.winfo_children():
widget.destroy()
# Group forecast by day
daily_forecasts = {}
for item in forecast_data['list'][:40]: # 5 days
date = datetime.fromtimestamp(item['dt']).date()
if date not in daily_forecasts:
daily_forecasts[date] = []
daily_forecasts[date].append(item)
# Display daily forecasts
for i, (date, forecasts) in enumerate(list(daily_forecasts.items())[:5]):
# Calculate daily min/max temperatures
temps = [f['main']['temp'] for f in forecasts]
min_temp = round(min(temps))
max_temp = round(max(temps))
# Get most common weather condition
conditions = [f['weather'][0]['main'] for f in forecasts]
most_common = max(set(conditions), key=conditions.count)
# Create forecast frame
day_frame = tk.Frame(self.forecast_frame, bg='#34495e')
day_frame.grid(row=0, column=i, padx=5, pady=5, sticky='nsew')
# Configure grid weights
self.forecast_frame.grid_columnconfigure(i, weight=1)
# Day label
day_name = date.strftime('%A') if date == datetime.now().date() else date.strftime('%a')
day_label = tk.Label(
day_frame,
text=day_name,
font=("Arial", 10, "bold"),
bg='#34495e',
fg='#ecf0f1'
)
day_label.pack(pady=2)
# Date label
date_label = tk.Label(
day_frame,
text=date.strftime('%m/%d'),
font=("Arial", 9),
bg='#34495e',
fg='#bdc3c7'
)
date_label.pack()
# Weather condition
condition_label = tk.Label(
day_frame,
text=most_common,
font=("Arial", 9),
bg='#34495e',
fg='#ecf0f1'
)
condition_label.pack(pady=2)
# Temperature range
temp_label = tk.Label(
day_frame,
text=f"{max_temp}°/{min_temp}°",
font=("Arial", 10, "bold"),
bg='#34495e',
fg='#e74c3c'
)
temp_label.pack()
def _show_error(self, message: str):
"""Show error message"""
messagebox.showerror("Error", message)
self.status_var.set("Error occurred")
self.search_button.config(state='normal')
def refresh_weather(self):
"""Refresh current weather data"""
city = self.city_entry.get().strip()
if city:
self.get_weather()
else:
messagebox.showwarning("Warning", "Please enter a city name first")
def save_weather_data(self):
"""Save current weather data to file"""
if not self.current_weather:
messagebox.showwarning("Warning", "No weather data to save")
return
try:
filename = f"weather_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
data_to_save = {
"current_weather": self.current_weather,
"forecast": self.forecast_data,
"saved_at": datetime.now().isoformat()
}
with open(filename, 'w') as f:
json.dump(data_to_save, f, indent=2)
messagebox.showinfo("Success", f"Weather data saved to {filename}")
self.status_var.set(f"Data saved to {filename}")
except Exception as e:
messagebox.showerror("Error", f"Failed to save data: {str(e)}")
def main():
"""Main function to run the weather app"""
root = tk.Tk()
app = WeatherApp(root)
# Center the window
root.update_idletasks()
width = root.winfo_width()
height = root.winfo_height()
x = (root.winfo_screenwidth() // 2) - (width // 2)
y = (root.winfo_screenheight() // 2) - (height // 2)
root.geometry(f'{width}x{height}+{x}+{y}')
# Show instructions for API key
messagebox.showinfo(
"API Key Required",
"To use real weather data, please:\n"
"1. Sign up at https://openweathermap.org/api\n"
"2. Get your free API key\n"
"3. Replace 'your_api_key_here' in the code\n\n"
"For now, the app will show demo data."
)
root.mainloop()
if __name__ == "__main__":
main()
# Weather App with GUI (Tkinter)
import tkinter as tk
from tkinter import ttk, messagebox
import requests
import json
from datetime import datetime, timedelta
from typing import Dict, Optional, List
import threading
class WeatherApp:
def __init__(self, root):
self.root = root
self.root.title("Weather App")
self.root.geometry("800x600")
self.root.configure(bg='#2c3e50')
# API key (you would get this from OpenWeatherMap)
self.api_key = "your_api_key_here" # Replace with actual API key
self.base_url = "http://api.openweathermap.org/data/2.5"
# Store weather data
self.current_weather = None
self.forecast_data = None
self.setup_ui()
def setup_ui(self):
"""Setup the user interface"""
# Title
title_label = tk.Label(
self.root,
text="Weather App",
font=("Arial", 24, "bold"),
bg='#2c3e50',
fg='#ecf0f1'
)
title_label.pack(pady=20)
# Search frame
search_frame = tk.Frame(self.root, bg='#2c3e50')
search_frame.pack(pady=10)
tk.Label(
search_frame,
text="Enter City:",
font=("Arial", 12),
bg='#2c3e50',
fg='#ecf0f1'
).pack(side=tk.LEFT, padx=5)
self.city_entry = tk.Entry(
search_frame,
font=("Arial", 12),
width=20
)
self.city_entry.pack(side=tk.LEFT, padx=5)
self.city_entry.bind('<Return>', lambda event: self.get_weather())
self.search_button = tk.Button(
search_frame,
text="Get Weather",
command=self.get_weather,
font=("Arial", 10, "bold"),
bg='#3498db',
fg='white',
padx=10
)
self.search_button.pack(side=tk.LEFT, padx=5)
# Current weather frame
self.current_frame = tk.LabelFrame(
self.root,
text="Current Weather",
font=("Arial", 14, "bold"),
bg='#34495e',
fg='#ecf0f1',
padx=20,
pady=15
)
self.current_frame.pack(pady=20, padx=20, fill='x')
# Weather info labels
self.weather_labels = {}
self.create_weather_labels()
# Forecast frame
self.forecast_frame = tk.LabelFrame(
self.root,
text="5-Day Forecast",
font=("Arial", 14, "bold"),
bg='#34495e',
fg='#ecf0f1',
padx=20,
pady=15
)
self.forecast_frame.pack(pady=10, padx=20, fill='both', expand=True)
# Buttons frame
buttons_frame = tk.Frame(self.root, bg='#2c3e50')
buttons_frame.pack(pady=10)
self.refresh_button = tk.Button(
buttons_frame,
text="Refresh",
command=self.refresh_weather,
font=("Arial", 10),
bg='#27ae60',
fg='white',
padx=15
)
self.refresh_button.pack(side=tk.LEFT, padx=5)
self.save_button = tk.Button(
buttons_frame,
text="Save Data",
command=self.save_weather_data,
font=("Arial", 10),
bg='#e67e22',
fg='white',
padx=15
)
self.save_button.pack(side=tk.LEFT, padx=5)
# Status bar
self.status_var = tk.StringVar()
self.status_var.set("Ready")
status_bar = tk.Label(
self.root,
textvariable=self.status_var,
relief=tk.SUNKEN,
anchor=tk.W,
bg='#2c3e50',
fg='#ecf0f1'
)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def create_weather_labels(self):
"""Create labels for weather information"""
# City and country
self.weather_labels['city'] = tk.Label(
self.current_frame,
text="City: -",
font=("Arial", 14, "bold"),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['city'].grid(row=0, column=0, columnspan=2, pady=5, sticky='w')
# Temperature
self.weather_labels['temp'] = tk.Label(
self.current_frame,
text="Temperature: -",
font=("Arial", 24, "bold"),
bg='#34495e',
fg='#e74c3c'
)
self.weather_labels['temp'].grid(row=1, column=0, columnspan=2, pady=10)
# Weather description
self.weather_labels['desc'] = tk.Label(
self.current_frame,
text="Description: -",
font=("Arial", 12),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['desc'].grid(row=2, column=0, columnspan=2, pady=5)
# Feels like
self.weather_labels['feels_like'] = tk.Label(
self.current_frame,
text="Feels like: -",
font=("Arial", 11),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['feels_like'].grid(row=3, column=0, pady=5, sticky='w')
# Humidity
self.weather_labels['humidity'] = tk.Label(
self.current_frame,
text="Humidity: -",
font=("Arial", 11),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['humidity'].grid(row=3, column=1, pady=5, sticky='w')
# Pressure
self.weather_labels['pressure'] = tk.Label(
self.current_frame,
text="Pressure: -",
font=("Arial", 11),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['pressure'].grid(row=4, column=0, pady=5, sticky='w')
# Wind speed
self.weather_labels['wind'] = tk.Label(
self.current_frame,
text="Wind: -",
font=("Arial", 11),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['wind'].grid(row=4, column=1, pady=5, sticky='w')
# Visibility
self.weather_labels['visibility'] = tk.Label(
self.current_frame,
text="Visibility: -",
font=("Arial", 11),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['visibility'].grid(row=5, column=0, pady=5, sticky='w')
# UV Index (if available)
self.weather_labels['uv'] = tk.Label(
self.current_frame,
text="UV Index: -",
font=("Arial", 11),
bg='#34495e',
fg='#ecf0f1'
)
self.weather_labels['uv'].grid(row=5, column=1, pady=5, sticky='w')
def get_weather(self):
"""Get weather data for the specified city"""
city = self.city_entry.get().strip()
if not city:
messagebox.showwarning("Warning", "Please enter a city name")
return
# Show loading status
self.status_var.set("Loading weather data...")
self.search_button.config(state='disabled')
# Use threading to prevent UI freezing
thread = threading.Thread(target=self._fetch_weather_data, args=(city,))
thread.daemon = True
thread.start()
def _fetch_weather_data(self, city: str):
"""Fetch weather data in a separate thread"""
try:
# Get current weather
current_url = f"{self.base_url}/weather?q={city}&appid={self.api_key}&units=metric"
# For demo purposes, we'll use mock data
# In real implementation, you would use: response = requests.get(current_url)
mock_current_data = self.get_mock_current_weather(city)
# Get forecast
# forecast_url = f"{self.base_url}/forecast?q={city}&appid={self.api_key}&units=metric"
mock_forecast_data = self.get_mock_forecast_data(city)
# Update UI in main thread
self.root.after(0, self._update_weather_display, mock_current_data, mock_forecast_data)
except Exception as e:
self.root.after(0, self._show_error, f"Error fetching weather data: {str(e)}")
def get_mock_current_weather(self, city: str) -> Dict:
"""Generate mock current weather data for demo"""
import random
temps = [15, 18, 22, 25, 28, 30, 12, 8, 5, 2]
conditions = ["Clear", "Cloudy", "Rainy", "Sunny", "Partly Cloudy", "Overcast"]
return {
"name": city.title(),
"sys": {"country": "XX"},
"main": {
"temp": random.choice(temps),
"feels_like": random.choice(temps) + random.randint(-3, 3),
"humidity": random.randint(30, 80),
"pressure": random.randint(1000, 1030)
},
"weather": [{
"main": random.choice(conditions),
"description": random.choice(conditions).lower()
}],
"wind": {
"speed": random.randint(5, 25)
},
"visibility": random.randint(5000, 10000),
"dt": int(datetime.now().timestamp())
}
def get_mock_forecast_data(self, city: str) -> Dict:
"""Generate mock forecast data for demo"""
import random
forecast_list = []
for i in range(40): # 5 days * 8 times per day (3-hour intervals)
date = datetime.now() + timedelta(hours=i*3)
temp = random.randint(10, 30)
forecast_list.append({
"dt": int(date.timestamp()),
"dt_txt": date.strftime("%Y-%m-%d %H:%M:%S"),
"main": {
"temp": temp,
"temp_min": temp - 2,
"temp_max": temp + 2
},
"weather": [{
"main": random.choice(["Clear", "Cloudy", "Rain"]),
"description": "demo weather"
}]
})
return {"list": forecast_list}
def _update_weather_display(self, current_data: Dict, forecast_data: Dict):
"""Update the weather display with fetched data"""
try:
self.current_weather = current_data
self.forecast_data = forecast_data
# Update current weather
city_country = f"{current_data['name']}, {current_data['sys']['country']}"
self.weather_labels['city'].config(text=f"City: {city_country}")
temp = round(current_data['main']['temp'])
self.weather_labels['temp'].config(text=f"{temp}°C")
desc = current_data['weather'][0]['description'].title()
self.weather_labels['desc'].config(text=f"Description: {desc}")
feels_like = round(current_data['main']['feels_like'])
self.weather_labels['feels_like'].config(text=f"Feels like: {feels_like}°C")
humidity = current_data['main']['humidity']
self.weather_labels['humidity'].config(text=f"Humidity: {humidity}%")
pressure = current_data['main']['pressure']
self.weather_labels['pressure'].config(text=f"Pressure: {pressure} hPa")
wind_speed = current_data['wind']['speed']
self.weather_labels['wind'].config(text=f"Wind: {wind_speed} m/s")
visibility = current_data.get('visibility', 0) / 1000
self.weather_labels['visibility'].config(text=f"Visibility: {visibility:.1f} km")
# Update forecast
self.update_forecast_display(forecast_data)
# Update status
self.status_var.set(f"Last updated: {datetime.now().strftime('%H:%M:%S')}")
except Exception as e:
self._show_error(f"Error updating display: {str(e)}")
finally:
self.search_button.config(state='normal')
def update_forecast_display(self, forecast_data: Dict):
"""Update the forecast display"""
# Clear existing forecast widgets
for widget in self.forecast_frame.winfo_children():
widget.destroy()
# Group forecast by day
daily_forecasts = {}
for item in forecast_data['list'][:40]: # 5 days
date = datetime.fromtimestamp(item['dt']).date()
if date not in daily_forecasts:
daily_forecasts[date] = []
daily_forecasts[date].append(item)
# Display daily forecasts
for i, (date, forecasts) in enumerate(list(daily_forecasts.items())[:5]):
# Calculate daily min/max temperatures
temps = [f['main']['temp'] for f in forecasts]
min_temp = round(min(temps))
max_temp = round(max(temps))
# Get most common weather condition
conditions = [f['weather'][0]['main'] for f in forecasts]
most_common = max(set(conditions), key=conditions.count)
# Create forecast frame
day_frame = tk.Frame(self.forecast_frame, bg='#34495e')
day_frame.grid(row=0, column=i, padx=5, pady=5, sticky='nsew')
# Configure grid weights
self.forecast_frame.grid_columnconfigure(i, weight=1)
# Day label
day_name = date.strftime('%A') if date == datetime.now().date() else date.strftime('%a')
day_label = tk.Label(
day_frame,
text=day_name,
font=("Arial", 10, "bold"),
bg='#34495e',
fg='#ecf0f1'
)
day_label.pack(pady=2)
# Date label
date_label = tk.Label(
day_frame,
text=date.strftime('%m/%d'),
font=("Arial", 9),
bg='#34495e',
fg='#bdc3c7'
)
date_label.pack()
# Weather condition
condition_label = tk.Label(
day_frame,
text=most_common,
font=("Arial", 9),
bg='#34495e',
fg='#ecf0f1'
)
condition_label.pack(pady=2)
# Temperature range
temp_label = tk.Label(
day_frame,
text=f"{max_temp}°/{min_temp}°",
font=("Arial", 10, "bold"),
bg='#34495e',
fg='#e74c3c'
)
temp_label.pack()
def _show_error(self, message: str):
"""Show error message"""
messagebox.showerror("Error", message)
self.status_var.set("Error occurred")
self.search_button.config(state='normal')
def refresh_weather(self):
"""Refresh current weather data"""
city = self.city_entry.get().strip()
if city:
self.get_weather()
else:
messagebox.showwarning("Warning", "Please enter a city name first")
def save_weather_data(self):
"""Save current weather data to file"""
if not self.current_weather:
messagebox.showwarning("Warning", "No weather data to save")
return
try:
filename = f"weather_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
data_to_save = {
"current_weather": self.current_weather,
"forecast": self.forecast_data,
"saved_at": datetime.now().isoformat()
}
with open(filename, 'w') as f:
json.dump(data_to_save, f, indent=2)
messagebox.showinfo("Success", f"Weather data saved to {filename}")
self.status_var.set(f"Data saved to {filename}")
except Exception as e:
messagebox.showerror("Error", f"Failed to save data: {str(e)}")
def main():
"""Main function to run the weather app"""
root = tk.Tk()
app = WeatherApp(root)
# Center the window
root.update_idletasks()
width = root.winfo_width()
height = root.winfo_height()
x = (root.winfo_screenwidth() // 2) - (width // 2)
y = (root.winfo_screenheight() // 2) - (height // 2)
root.geometry(f'{width}x{height}+{x}+{y}')
# Show instructions for API key
messagebox.showinfo(
"API Key Required",
"To use real weather data, please:\n"
"1. Sign up at https://openweathermap.org/api\n"
"2. Get your free API key\n"
"3. Replace 'your_api_key_here' in the code\n\n"
"For now, the app will show demo data."
)
root.mainloop()
if __name__ == "__main__":
main()
- Replace
your_api_key_here
your_api_key_here
with your actual OpenWeatherMap API key. - Save the file.
- Run the following command to run the application.
C:\Users\username\Documents\weatherAppGUI> python weatherappgui.py
Weather App
Enter City: New York
[Click "Get Weather"]
Current Weather in New York, US
Temperature: 22°C
Description: Clear Sky
Humidity: 65%, Wind: 8 m/s
5-Day Forecast displayed with daily temperatures
C:\Users\username\Documents\weatherAppGUI> python weatherappgui.py
Weather App
Enter City: New York
[Click "Get Weather"]
Current Weather in New York, US
Temperature: 22°C
Description: Clear Sky
Humidity: 65%, Wind: 8 m/s
5-Day Forecast displayed with daily temperatures
Explanation
- The
import tkinter as tk
import tkinter as tk
statement imports the Tkinter library for creating the GUI interface. - The
import requests
import requests
imports the requests library for making HTTP API calls to OpenWeatherMap. - The
class WeatherApp:
class WeatherApp:
defines the main application class that manages the GUI and weather data. - The
setup_ui()
setup_ui()
method creates all the GUI components including entry fields, buttons, and display labels. - The
get_weather()
get_weather()
method handles user input and initiates the weather data fetching process. - The threading implementation prevents the UI from freezing during API calls.
- The
_fetch_weather_data()
_fetch_weather_data()
method makes API requests to get current weather and forecast data. - The weather display updates show current conditions, temperature, humidity, wind speed, and pressure.
- The forecast display shows a 5-day weather prediction with daily temperature ranges.
- The
save_weather_data()
save_weather_data()
method allows users to save weather information to JSON files. - Error handling manages network issues, invalid city names, and API response errors.
- The status bar provides real-time feedback about the application’s current state.
Next Steps
Congratulations! You have successfully created a Weather App GUI in Python. Experiment with the code and see if you can modify the application. Here are a few suggestions:
- Add weather maps and radar imagery
- Implement location-based weather using GPS coordinates
- Create weather alerts and notifications
- Add multiple city comparison features
- Implement weather history charts and graphs
- Add weather widgets for desktop integration
- Create different color themes based on weather conditions
- Add voice announcements for weather updates
- Implement weather-based activity recommendations
Conclusion
In this project, you learned how to create a Weather App GUI in Python using Tkinter and API integration. You also learned about threading, data persistence, real-time data fetching, and creating responsive user interfaces. You can find the source code on GitHub
1. WeatherApp Class Structure
class WeatherApp:
def __init__(self):
self.root = tk.Tk()
self.api_key = "YOUR_API_KEY_HERE"
self.base_url = "http://api.openweathermap.org/data/2.5/"
self.weather_data = {}
self.favorites = []
class WeatherApp:
def __init__(self):
self.root = tk.Tk()
self.api_key = "YOUR_API_KEY_HERE"
self.base_url = "http://api.openweathermap.org/data/2.5/"
self.weather_data = {}
self.favorites = []
The main class manages:
- GUI Components: All Tkinter widgets and layouts
- API Communication: HTTP requests to OpenWeatherMap
- Data Management: Weather data storage and retrieval
- User Preferences: Favorite cities and settings
2. API Integration
def get_weather_data(self, city, units="metric"):
"""Fetch current weather data from API"""
try:
url = f"{self.base_url}weather"
params = {
"q": city,
"appid": self.api_key,
"units": units
}
response = requests.get(url, params=params, timeout=10)
return response.json()
except requests.RequestException as e:
self.show_error(f"API Error: {e}")
return None
def get_weather_data(self, city, units="metric"):
"""Fetch current weather data from API"""
try:
url = f"{self.base_url}weather"
params = {
"q": city,
"appid": self.api_key,
"units": units
}
response = requests.get(url, params=params, timeout=10)
return response.json()
except requests.RequestException as e:
self.show_error(f"API Error: {e}")
return None
3. Threading for Responsive UI
def fetch_weather_threaded(self, city):
"""Fetch weather data in separate thread"""
def worker():
self.show_loading(True)
data = self.get_weather_data(city)
self.root.after(0, lambda: self.display_weather(data))
self.show_loading(False)
thread = threading.Thread(target=worker, daemon=True)
thread.start()
def fetch_weather_threaded(self, city):
"""Fetch weather data in separate thread"""
def worker():
self.show_loading(True)
data = self.get_weather_data(city)
self.root.after(0, lambda: self.display_weather(data))
self.show_loading(False)
thread = threading.Thread(target=worker, daemon=True)
thread.start()
4. Data Persistence
def save_weather_data(self, city, data):
"""Save weather data locally"""
filename = f"weather_data_{city.lower()}.json"
try:
with open(filename, 'w') as f:
json.dump(data, f, indent=2)
except IOError as e:
self.show_error(f"Save Error: {e}")
def save_weather_data(self, city, data):
"""Save weather data locally"""
filename = f"weather_data_{city.lower()}.json"
try:
with open(filename, 'w') as f:
json.dump(data, f, indent=2)
except IOError as e:
self.show_error(f"Save Error: {e}")
Usage Examples
Basic Usage
# Run the application
app = WeatherApp()
app.run()
# Run the application
app = WeatherApp()
app.run()
API Configuration
# Set your API key
app = WeatherApp()
app.set_api_key("your_openweathermap_api_key")
app.run()
# Set your API key
app = WeatherApp()
app.set_api_key("your_openweathermap_api_key")
app.run()
Custom Settings
# Configure default settings
app = WeatherApp()
app.set_default_units("imperial") # Fahrenheit, mph
app.set_default_city("New York")
app.run()
# Configure default settings
app = WeatherApp()
app.set_default_units("imperial") # Fahrenheit, mph
app.set_default_city("New York")
app.run()
Running the Application
Command Line
python weatherappgui.py
python weatherappgui.py
Expected Output
Weather App Starting...
API Key: Configured ✓
GUI: Initialized ✓
Ready for use!
Weather App Starting...
API Key: Configured ✓
GUI: Initialized ✓
Ready for use!
User Interface Guide
Main Window Components
1. Search Section
- City Input: Text field for entering city names
- Search Button: Fetch weather for entered city
- Units Toggle: Switch between Celsius/Fahrenheit
2. Current Weather Display
Current Weather in New York
Temperature: 22°C (Feels like 25°C)
Condition: Partly Cloudy
Humidity: 65%
Wind: 15 km/h NE
Pressure: 1013 hPa
Visibility: 10 km
UV Index: 6 (High)
Current Weather in New York
Temperature: 22°C (Feels like 25°C)
Condition: Partly Cloudy
Humidity: 65%
Wind: 15 km/h NE
Pressure: 1013 hPa
Visibility: 10 km
UV Index: 6 (High)
3. Forecast Section
- 5-Day Forecast: Daily weather predictions
- Hourly Forecast: 24-hour detailed forecast
- Weather Charts: Temperature and precipitation graphs
4. Additional Features
- Favorites List: Quick access to saved cities
- Weather History: Previously fetched data
- Settings Panel: Customize app preferences
Advanced Features
1. Weather Alerts
def check_weather_alerts(self, weather_data):
"""Check for severe weather conditions"""
alerts = []
# Temperature alerts
temp = weather_data['main']['temp']
if temp > 35: # Celsius
alerts.append("⚠️ Extreme Heat Warning")
elif temp < -10:
alerts.append("❄️ Extreme Cold Warning")
# Wind alerts
wind_speed = weather_data['wind']['speed']
if wind_speed > 20: # m/s
alerts.append("💨 High Wind Warning")
return alerts
def check_weather_alerts(self, weather_data):
"""Check for severe weather conditions"""
alerts = []
# Temperature alerts
temp = weather_data['main']['temp']
if temp > 35: # Celsius
alerts.append("⚠️ Extreme Heat Warning")
elif temp < -10:
alerts.append("❄️ Extreme Cold Warning")
# Wind alerts
wind_speed = weather_data['wind']['speed']
if wind_speed > 20: # m/s
alerts.append("💨 High Wind Warning")
return alerts
2. Data Visualization
def create_temperature_chart(self, forecast_data):
"""Create temperature trend chart"""
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
# Extract temperature data
temps = [item['main']['temp'] for item in forecast_data]
times = [item['dt_txt'] for item in forecast_data]
# Create chart
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(times, temps, marker='o')
ax.set_title('Temperature Forecast')
ax.set_ylabel('Temperature (°C)')
# Embed in Tkinter
canvas = FigureCanvasTkAgg(fig, self.chart_frame)
canvas.draw()
canvas.get_tk_widget().pack()
def create_temperature_chart(self, forecast_data):
"""Create temperature trend chart"""
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
# Extract temperature data
temps = [item['main']['temp'] for item in forecast_data]
times = [item['dt_txt'] for item in forecast_data]
# Create chart
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(times, temps, marker='o')
ax.set_title('Temperature Forecast')
ax.set_ylabel('Temperature (°C)')
# Embed in Tkinter
canvas = FigureCanvasTkAgg(fig, self.chart_frame)
canvas.draw()
canvas.get_tk_widget().pack()
3. Favorites Management
def add_to_favorites(self, city):
"""Add city to favorites list"""
if city not in self.favorites:
self.favorites.append(city)
self.save_favorites()
self.update_favorites_list()
def save_favorites(self):
"""Persist favorites to file"""
with open('favorites.json', 'w') as f:
json.dump(self.favorites, f)
def add_to_favorites(self, city):
"""Add city to favorites list"""
if city not in self.favorites:
self.favorites.append(city)
self.save_favorites()
self.update_favorites_list()
def save_favorites(self):
"""Persist favorites to file"""
with open('favorites.json', 'w') as f:
json.dump(self.favorites, f)
Configuration Options
Settings File (config.json)
{
"api_key": "your_api_key_here",
"default_city": "London",
"default_units": "metric",
"update_interval": 300,
"theme": "light",
"show_forecast": true,
"auto_refresh": true
}
{
"api_key": "your_api_key_here",
"default_city": "London",
"default_units": "metric",
"update_interval": 300,
"theme": "light",
"show_forecast": true,
"auto_refresh": true
}
Customization Examples
# Theme customization
def apply_dark_theme(self):
"""Apply dark theme to UI"""
self.root.configure(bg='#2b2b2b')
self.main_frame.configure(bg='#2b2b2b')
# Update all widget colors
# Unit conversion
def convert_temperature(self, temp, from_unit, to_unit):
"""Convert temperature between units"""
if from_unit == "celsius" and to_unit == "fahrenheit":
return (temp * 9/5) + 32
elif from_unit == "fahrenheit" and to_unit == "celsius":
return (temp - 32) * 5/9
return temp
# Theme customization
def apply_dark_theme(self):
"""Apply dark theme to UI"""
self.root.configure(bg='#2b2b2b')
self.main_frame.configure(bg='#2b2b2b')
# Update all widget colors
# Unit conversion
def convert_temperature(self, temp, from_unit, to_unit):
"""Convert temperature between units"""
if from_unit == "celsius" and to_unit == "fahrenheit":
return (temp * 9/5) + 32
elif from_unit == "fahrenheit" and to_unit == "celsius":
return (temp - 32) * 5/9
return temp
Sample Weather Data
Current Weather Response
{
"weather": [
{
"id": 801,
"main": "Clouds",
"description": "few clouds",
"icon": "02d"
}
],
"main": {
"temp": 22.5,
"feels_like": 24.8,
"temp_min": 20.1,
"temp_max": 25.3,
"pressure": 1013,
"humidity": 65
},
"wind": {
"speed": 4.2,
"deg": 180
},
"dt": 1693737600,
"name": "London"
}
{
"weather": [
{
"id": 801,
"main": "Clouds",
"description": "few clouds",
"icon": "02d"
}
],
"main": {
"temp": 22.5,
"feels_like": 24.8,
"temp_min": 20.1,
"temp_max": 25.3,
"pressure": 1013,
"humidity": 65
},
"wind": {
"speed": 4.2,
"deg": 180
},
"dt": 1693737600,
"name": "London"
}
Forecast Response Structure
{
"list": [
{
"dt": 1693737600,
"main": {
"temp": 22.5,
"humidity": 65
},
"weather": [
{
"main": "Clouds",
"description": "few clouds"
}
],
"dt_txt": "2025-09-02 12:00:00"
}
]
}
{
"list": [
{
"dt": 1693737600,
"main": {
"temp": 22.5,
"humidity": 65
},
"weather": [
{
"main": "Clouds",
"description": "few clouds"
}
],
"dt_txt": "2025-09-02 12:00:00"
}
]
}
Error Handling
Common Issues and Solutions
1. API Key Issues
def validate_api_key(self):
"""Validate API key before making requests"""
test_url = f"{self.base_url}weather?q=London&appid={self.api_key}"
try:
response = requests.get(test_url, timeout=5)
if response.status_code == 401:
raise ValueError("Invalid API key")
return True
except requests.RequestException:
return False
def validate_api_key(self):
"""Validate API key before making requests"""
test_url = f"{self.base_url}weather?q=London&appid={self.api_key}"
try:
response = requests.get(test_url, timeout=5)
if response.status_code == 401:
raise ValueError("Invalid API key")
return True
except requests.RequestException:
return False
2. Network Connectivity
def check_internet_connection(self):
"""Check if internet connection is available"""
try:
requests.get('https://www.google.com', timeout=3)
return True
except requests.RequestException:
return False
def check_internet_connection(self):
"""Check if internet connection is available"""
try:
requests.get('https://www.google.com', timeout=3)
return True
except requests.RequestException:
return False
3. Invalid City Names
def validate_city_name(self, city):
"""Validate city name format"""
if not city or len(city.strip()) < 2:
raise ValueError("City name too short")
# Check for valid characters
import re
if not re.match(r'^[a-zA-Z\s\-\.]+$', city):
raise ValueError("Invalid characters in city name")
return city.strip().title()
def validate_city_name(self, city):
"""Validate city name format"""
if not city or len(city.strip()) < 2:
raise ValueError("City name too short")
# Check for valid characters
import re
if not re.match(r'^[a-zA-Z\s\-\.]+$', city):
raise ValueError("Invalid characters in city name")
return city.strip().title()
Data Export Features
Export to CSV
def export_weather_data(self, filename="weather_export.csv"):
"""Export weather data to CSV"""
import csv
with open(filename, 'w', newline='') as csvfile:
fieldnames = ['city', 'date', 'temperature', 'humidity', 'description']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for record in self.weather_history:
writer.writerow(record)
def export_weather_data(self, filename="weather_export.csv"):
"""Export weather data to CSV"""
import csv
with open(filename, 'w', newline='') as csvfile:
fieldnames = ['city', 'date', 'temperature', 'humidity', 'description']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for record in self.weather_history:
writer.writerow(record)
Weather Analytics
def analyze_weather_trends(self, city):
"""Analyze weather trends for a city"""
data = self.load_weather_history(city)
if not data:
return None
temps = [record['temperature'] for record in data]
analysis = {
'average_temp': sum(temps) / len(temps),
'max_temp': max(temps),
'min_temp': min(temps),
'temp_range': max(temps) - min(temps)
}
return analysis
def analyze_weather_trends(self, city):
"""Analyze weather trends for a city"""
data = self.load_weather_history(city)
if not data:
return None
temps = [record['temperature'] for record in data]
analysis = {
'average_temp': sum(temps) / len(temps),
'max_temp': max(temps),
'min_temp': min(temps),
'temp_range': max(temps) - min(temps)
}
return analysis
Troubleshooting
Common Problems
1. API Rate Limiting
# Solution: Implement rate limiting
import time
def make_api_request_with_rate_limit(self, url, params):
"""Make API request with rate limiting"""
if hasattr(self, 'last_request_time'):
time_since_last = time.time() - self.last_request_time
if time_since_last < 1: # 1 second minimum between requests
time.sleep(1 - time_since_last)
self.last_request_time = time.time()
return requests.get(url, params=params)
# Solution: Implement rate limiting
import time
def make_api_request_with_rate_limit(self, url, params):
"""Make API request with rate limiting"""
if hasattr(self, 'last_request_time'):
time_since_last = time.time() - self.last_request_time
if time_since_last < 1: # 1 second minimum between requests
time.sleep(1 - time_since_last)
self.last_request_time = time.time()
return requests.get(url, params=params)
2. GUI Freezing
# Solution: Use threading for all API calls
def update_weather_async(self, city):
"""Update weather data asynchronously"""
def fetch_and_update():
data = self.get_weather_data(city)
# Use root.after to update GUI from main thread
self.root.after(0, lambda: self.update_display(data))
threading.Thread(target=fetch_and_update, daemon=True).start()
# Solution: Use threading for all API calls
def update_weather_async(self, city):
"""Update weather data asynchronously"""
def fetch_and_update():
data = self.get_weather_data(city)
# Use root.after to update GUI from main thread
self.root.after(0, lambda: self.update_display(data))
threading.Thread(target=fetch_and_update, daemon=True).start()
3. Memory Usage
# Solution: Limit data storage
def manage_data_storage(self):
"""Manage memory usage by limiting stored data"""
max_records = 1000
if len(self.weather_history) > max_records:
# Keep only recent records
self.weather_history = self.weather_history[-max_records:]
# Solution: Limit data storage
def manage_data_storage(self):
"""Manage memory usage by limiting stored data"""
max_records = 1000
if len(self.weather_history) > max_records:
# Keep only recent records
self.weather_history = self.weather_history[-max_records:]
Extensions and Improvements
1. Weather Maps Integration
def show_weather_map(self, city):
"""Display weather map for city"""
# Integrate with map services
map_url = f"https://tile.openweathermap.org/map/temp_new/5/{lat}/{lon}.png"
# Display map in new window
def show_weather_map(self, city):
"""Display weather map for city"""
# Integrate with map services
map_url = f"https://tile.openweathermap.org/map/temp_new/5/{lat}/{lon}.png"
# Display map in new window
2. Push Notifications
def setup_weather_notifications(self):
"""Setup desktop notifications for weather alerts"""
import plyer
def notify_weather_alert(message):
plyer.notification.notify(
title="Weather Alert",
message=message,
timeout=10
)
def setup_weather_notifications(self):
"""Setup desktop notifications for weather alerts"""
import plyer
def notify_weather_alert(message):
plyer.notification.notify(
title="Weather Alert",
message=message,
timeout=10
)
3. Multiple Language Support
def set_language(self, lang_code):
"""Set application language"""
self.language = lang_code
self.load_translations()
self.update_ui_text()
def set_language(self, lang_code):
"""Set application language"""
self.language = lang_code
self.load_translations()
self.update_ui_text()
Performance Optimization
1. Caching Strategy
import time
def get_weather_with_cache(self, city):
"""Get weather data with caching"""
cache_key = f"weather_{city}"
cache_duration = 300 # 5 minutes
if cache_key in self.cache:
cached_data, timestamp = self.cache[cache_key]
if time.time() - timestamp < cache_duration:
return cached_data
# Fetch fresh data
data = self.get_weather_data(city)
self.cache[cache_key] = (data, time.time())
return data
import time
def get_weather_with_cache(self, city):
"""Get weather data with caching"""
cache_key = f"weather_{city}"
cache_duration = 300 # 5 minutes
if cache_key in self.cache:
cached_data, timestamp = self.cache[cache_key]
if time.time() - timestamp < cache_duration:
return cached_data
# Fetch fresh data
data = self.get_weather_data(city)
self.cache[cache_key] = (data, time.time())
return data
2. Background Updates
def start_background_updates(self):
"""Start background weather updates"""
def update_worker():
while self.running:
for city in self.favorites:
self.update_weather_cache(city)
time.sleep(600) # Update every 10 minutes
self.update_thread = threading.Thread(target=update_worker, daemon=True)
self.update_thread.start()
def start_background_updates(self):
"""Start background weather updates"""
def update_worker():
while self.running:
for city in self.favorites:
self.update_weather_cache(city)
time.sleep(600) # Update every 10 minutes
self.update_thread = threading.Thread(target=update_worker, daemon=True)
self.update_thread.start()
Next Steps
After completing this weather app, consider:
- Mobile Development: Create mobile versions with Kivy or BeeWare
- Web Version: Build web interface with Flask or Django
- Machine Learning: Add weather prediction models
- IoT Integration: Connect with weather sensors
- Advanced Visualizations: Use Plotly for interactive charts
Resources
- OpenWeatherMap API Documentation
- Tkinter Documentation
- Python Threading
- Requests Library
- JSON Handling in Python
Conclusion
This weather application demonstrates professional GUI development with Python. It showcases important concepts like API integration, threading, data persistence, and user interface design. The modular structure makes it easy to extend with additional features and customizations.
The application provides a solid foundation for building more complex desktop applications and demonstrates best practices for handling external APIs and creating responsive user interfaces. 🌤️🐍
Was this page helpful?
Let us know how we did