Build a Weather App
Develop a weather app using external APIs — Mobile App Development.
Focus: develop a weather app using external apis
Building a weather app sounds deceptively simple: you need a location, a weather service, and a screen to show results. But in practice, every beginner hits the same wall — the API returns ugly JSON, the network call blocks the UI, and the app crashes on a slow connection. This lesson walks you through developing a weather app using external APIs step by step, so you go from a frozen screen to a smooth, production-ready experience. You'll learn how to fetch live data, parse it safely, handle errors gracefully, and structure your code so it works on any platform with Python frameworks like Kivy or BeeWare.
The problem this lesson solves
Most tutorials show you how to paste a URL and print a response — then reality hits. Your weather app needs to fetch data over the network, parse it, and display it — all without freezing or crashing. Here's what usually goes wrong when developers first integrate an external API:
- The UI freezes because the network call runs on the main thread.
- The app crashes when the API returns unexpected data (or nothing at all).
- The API key leaks into version control, getting you banned.
- The wrong city's weather shows up because geocoding was skipped.
- Rate limits hit you when your app polls too often.
If you're learning mobile app development, these are not edge cases — they will happen on the first real device test. This lesson gives you a battle-tested pathway to handle all of them, using a simple weather API as your case study.
Core concept / mental model
Think of a weather app as a three-layer sandwich:
- Data layer — talks to the external API, retrieves JSON, parses it.
- Business logic layer — decides what to do with the data (e.g., convert units, decide if it's raining).
- UI layer — renders the result to the user.
Each layer has one job and doesn't peek into the others. This is the Model-View-Controller (MVC) pattern adapted for mobile. The API is your raw ingredient; your app is the kitchen that turns it into a meal (the UI).
Key terms you'll use constantly:
- API endpoint — a specific URL that returns data (e.g.,
https://api.openweathermap.org/data/2.5/weather). - API key — your personal token to access the service; treat it like a password.
- JSON — the data format most modern APIs return (a tree of key-value pairs).
- HTTP method —
GET(retrieve data),POST(send data), etc. For weather, you'll useGET. - Asynchronous call — code that doesn't block the UI while waiting for the network.
- Rate limit — how many requests you can make per minute/hour; exceeding it gets you blocked.
Here's a simple diagram of the flow:
[Your App] --(1. send request)--> [External API]
^ |
| (2. return JSON) |
+----------------------------------+
Now let's see how to implement each layer.
How it works step by step
Step 1: Choose a weather API and sign up. The most common free tier is OpenWeatherMap. You'll get an API key — a long string like 1234abc... that identifies your app.
Step 2: Understand the API contract. Read the docs to find the endpoint, required parameters (q for city, appid for key, units for temperature scale), and sample response. For example, a GET to https://api.openweathermap.org/data/2.5/weather?q=London&appid=YOUR_KEY&units=metric returns JSON like:
{
"weather": [{"main": "Clouds", "description": "scattered clouds"}],
"main": {"temp": 17.2, "humidity": 72},
"name": "London"
}
Step 3: Build the data layer in Python. Use the requests library to fetch data. Create a function get_weather(city) that returns a parsed dictionary. Always include a timeout and wrap the call in a try/except.
Step 4: Make it asynchronous. In Kivy, use the Clock scheduler or a thread to run the network call without blocking the UI. BeeWare's Toga supports async too. Never call sleep() on the main thread.
Step 5: Parse and map data to your model. Extract only the fields you need (temp, humidity, description). Convert units if needed.
Step 6: Update the UI. Send the parsed data to the UI layer to update labels or icons. Use a callback or a thread-safe event.
Step 7: Handle errors. If the city isn't found, the API returns a 404. If the key is invalid, a 401. Always show a friendly message to the user, never a stack trace.
Step 8: Manage your API key securely. Store it in environment variables or a config file that's not committed to git. On a device, use platform-specific secure storage.
Hands-on walkthrough
Let's build a minimal weather app with Kivy (Python's cross‑platform UI framework) and the requests library. First, install dependencies:
pip install kivy requests
1. The data layer (backend)
Create a file weather_api.py:
import requests
from typing import Dict, Optional
API_KEY = "YOUR_API_KEY" # Better: os.getenv("OPENWEATHER_API_KEY")
BASE_URL = "https://api.openweathermap.org/data/2.5/weather"
def get_weather(city: str, units: str = "metric") -> Optional[Dict[str, str]]:
"""Fetch current weather for a city and return a simplified dict."""
params = {"q": city, "appid": API_KEY, "units": units}
try:
response = requests.get(BASE_URL, params=params, timeout=5)
response.raise_for_status() # Raise an error for 4xx/5xx
data = response.json()
return {
"city": data["name"],
"temperature": data["main"]["temp"],
"humidity": data["main"]["humidity"],
"description": data["weather"][0]["description"],
}
except (requests.exceptions.RequestException, KeyError, ValueError):
return None
Expected output (when called):
print(get_weather("London"))
# {'city': 'London', 'temperature': 17.2, 'humidity': 72, 'description': 'scattered clouds'}
2. The UI layer (Kivy)
The UI lives on the main thread. To avoid freezing it, run the network call in a background thread, then update the labels via Clock.
import kivy
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
from kivy.uix.textinput import TextInput
from kivy.uix.button import Button
from kivy.clock import Clock
from weather_api import get_weather
import threading
class WeatherUI(BoxLayout):
def __init__(self, **kwargs):
super().__init__(orientation="vertical", spacing=10, padding=20, **kwargs)
self.city_input = TextInput(hint_text="Enter city name")
self.fetch_btn = Button(text="Get Weather")
self.fetch_btn.bind(on_press=self.on_fetch_click)
self.result_label = Label(text="Weather will appear here", halign="center")
self.add_widget(self.city_input)
self.add_widget(self.fetch_btn)
self.add_widget(self.result_label)
def on_fetch_click(self, instance):
city = self.city_input.text.strip()
if not city:
self.result_label.text = "Please enter a city."
return
self.fetch_btn.disabled = True
self.result_label.text = "Loading..."
# Start background thread
threading.Thread(target=self.fetch_and_update, args=(city,), daemon=True).start()
def fetch_and_update(self, city):
result = get_weather(city)
# Update UI on the main thread via Clock
Clock.schedule_once(lambda dt: self.display_result(result), 0)
def display_result(self, data):
self.fetch_btn.disabled = False
if data is None:
self.result_label.text = "Error: Could not fetch weather."
else:
self.result_label.text = (f"{data['city']}: {data['temperature']}°C, "
f"{data['description']}, humidity {data['humidity']}%")
class WeatherApp(App):
def build(self):
return WeatherUI()
if __name__ == "__main__":
WeatherApp().run()
Expected result: A window with a text input, a button, and a label. When you type a city and click, the label shows the weather after a second. The UI stays responsive (try dragging the window while loading).
3. Async alternative (modern approach)
If you prefer asyncio (used by BeeWare too), here's a more elegant pattern:
import asyncio
import aiohttp
async def get_weather_async(city: str) -> dict | None:
url = f"https://api.openweathermap.org/data/2.5/weather"
params = {"q": city, "appid": API_KEY, "units": "metric"}
async with aiohttp.ClientSession() as session:
try:
async with session.get(url, params=params, timeout=5) as resp:
if resp.status != 200:
return None
data = await resp.json()
return {
"city": data["name"],
"temperature": data["main"]["temp"],
"humidity": data["main"]["humidity"],
"description": data["weather"][0]["description"],
}
except (aiohttp.ClientError, KeyError) as e:
print(f"Network error: {e}")
return None
Pro tip: Always set a
timeout(e.g., 5 seconds) so your app doesn’t hang forever on a bad network. Users will appreciate a quick “Could not connect” message over an eternal spinner.
Compare options / when to choose what
| Feature | OpenWeatherMap | WeatherAPI.com | Tomorrow.io |
|---|---|---|---|
| Free tier limit | 60 calls/min, 1M/month | 1M calls/month | 500 calls/day |
| Requires API key? | Yes | Yes | Yes |
| Historical data | Limited on free | Yes (up to 7 days) | Yes |
| Minutely forecast | Paid | Paid | Yes (free tier) |
| Ease of use | Very easy | Easy | Moderate |
| Best for | Quick dashboards, tutorials | Hobby projects, demos | Real-time alerts, serious apps |
When to choose what:
- OpenWeatherMap — perfect for learning and MVP because the API is simple and well documented.
- WeatherAPI.com — great if you need more forecast days on the free tier.
- Tomorrow.io — choose when you need granular alerts or hyperlocal data; it has more parameters to tweak.
Alternative approaches to network calls:
- Requests + Threading (used above) — simple, works everywhere, but you manage the thread yourself.
- Asyncio + aiohttp — efficient, modern, and integrates well with Toga’s event loop, but adds async keyword complexity.
- Kivy’s built-in UrlRequest — actually asynchronous and Kivy-native, but less low-level control. Here’s a minimal version:
from kivy.network.urlrequest import UrlRequest
def fetch_city(city):
url = f"{BASE_URL}?q={city}&appid={API_KEY}&units=metric"
req = UrlRequest(url, on_success=on_success, on_error=on_error)
def on_success(req, result):
# result is a dict already
print("Temperature:", result["main"]["temp"])
def on_error(req, error):
print("Request failed:", error)
This is a solid option if you’re building a Kivy-only app and want to avoid threading.
Troubleshooting & edge cases
Let’s tackle the most common issues you’ll run into when developing your weather app.
1. App freezes on button click
Cause: You called get_weather() directly in the button callback, blocking the UI thread.
Fix: Use a thread or UrlRequest. In your UI code, never call network functions on the main thread.
2. response.json() throws a ValueError
Cause: The API returned an HTML error page (e.g., 404 or 500) or empty body, not JSON.
Fix: Always check response.status_code first, or wrap the JSON call in try/except. If the API is down, provide a fallback message.
3. 403 Forbidden or 401 Unauthorized
Cause: Your API key is missing or incorrect — or you didn’t store it in the right place.
Fix: Double-check your key, and store it in an environment variable. Never hardcode it in the app; if your code is committed to GitHub, the key leaks and gets misused.
4. The city name is misspelled or has spaces
Cause: Directly concatenating user input into a URL.
Fix: Use params in requests.get() as shown — the library handles URL encoding. Or call quote() from urllib.parse.
5. Rate limiting (429 Too Many Requests)
Cause: Your app polls the API too frequently (e.g., in a while loop or on every UI update).
Fix: Cache the response for a minimum interval (e.g., 5 minutes), and set a reasonable poll rate. Show the user the last-known data.
6. App displays None or crashes when keys are missing
Cause: The API changed the response structure, or you’re assuming a nested key that doesn’t exist.
Fix: Use .get() with safe navigation or check for key presence before indexing. For example:
temperature = data.get("main", {}).get("temp")
if temperature is None:
# handle gracefully
What you learned & what's next
You now know how to develop a weather app using external APIs — from choosing an API, building a data layer, making asynchronous calls, and handling errors, to comparing service providers. You’ve also seen how to keep your UI snappy and your code secure.
Key takeaways from this lesson:
- The three-layer architecture (data, logic, UI) keeps your app maintainable.
- Always fetch network data off the main thread.
- Validate and parse API responses defensively.
- Use environment variables to store API keys — never commit them.
Next in the track is likely about offline caching — storing weather data locally so your app works even when the network drops. That’s a perfect follow-up to lock in the patterns you just learned.
Go ahead: extend your app to cache the last fetch, add a 5-minute refresh button, and test on a real device with a spotty connection. You’ll feel the difference a thoughtful architecture makes.
Practice recap
Practice recap: Extend the Kivy app to add a 5-minute cache: save the last successful response in memory and display it immediately when the user re-queries the same city (indicate it's 'cached'). Then add a manual refresh button that force-updates from the API. Test with airplane mode on/off to verify your error handling works.
Common mistakes
- Blocking the UI thread: calling
requests.get()directly inside the button callback freezes the app. Always use a thread or async. - Hardcoding the API key in the source file and committing to version control — it gets exposed publicly and your key gets revoked.
- Assuming the API response always has every key; use
.get()ortry/except KeyErrorto avoid crashes on unexpected data. - Forgetting to set a timeout on the HTTP request, so the app hangs indefinitely on a dead network.
- Not checking HTTP status codes — a 404 city or 401 bad key will still attempt to parse a non-JSON error body and crash.
Variations
- Use different weather providers like OpenWeatherMap, WeatherAPI.com, or Tomorrow.io — each has distinct rate limits and data fields.
- Replace manual threading with Kivy’s built-in
UrlRequestfor simpler async handling in Kivy apps. - Adopt an
asyncio+aiohttppattern for modern Python apps, especially if you plan to add more API calls in parallel.
Real-world use cases
- A travel app that shows live weather at the destination when a user books a flight or hotel.
- An agricultural monitoring app that checks local rainfall and temperature to advise farmers on irrigation.
- A smart home dashboard that displays current conditions and alerts users if a storm is approaching a saved location.
Key takeaways
- Architect your app as three layers — data, logic, and UI — to keep network code isolated and testable.
- Perform every network request asynchronously to keep your mobile UI responsive and avoid ANR or UI lockups.
- Parse and validate API responses defensively: check status codes, wrap JSON in try/except, and use
.get()for optional keys. - Store API keys in environment variables or secure storage, never hardcode them in version‑controlled code.
- Compare API providers on rate limits, free tier, and data richness before committing to one for your app.
- Cache responses and respect rate limits to avoid 429 errors and ensure your app remains reliable.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.