Python Weather Application
Table of Contents + −
In the last lesson, Student Management System, you built a full CRUD app that stores its own data. So far all our projects worked with data we made ourselves. Now we reach out to the internet and pull in live data that someone else provides. We are building a weather app. You type a city, and the app asks an online service for the current weather there and shows it. You will use the requests library and JSON parsing from the APIs module, now inside a complete program you can run.
🤔 How does an app get live weather?
Your program does not measure the weather itself, right? It has no thermometer. So it asks a service that already has the data. That service offers an API, which is just a web address you send a request to and get data back from. You met APIs earlier; here we put one to real use.
The flow is always the same:
- Your program sends a request to the weather service. The city name goes along with it.
- The service sends back the weather data. It comes as JSON text, which is just data written as text in a structured way.
- Your program reads that JSON and shows the parts you care about, like the temperature.
So think of it like asking a friend who is standing outside. You ask “how’s the weather in London?”, and they reply with the details. Your code is the one asking, and the API is the friend with the answer.
For this project we will use OpenWeatherMap. It is a popular weather service with a free tier, so you can build and test without paying. To use it you need an API key, which is a secret code that identifies you. You sign up once on their website and they give you a key. The key proves you are allowed to use the service, and it lets them count how many requests you make.
Note
Sign up for a free OpenWeatherMap account and you will find an API key on your account page. A new key can take a little while to start working, so do not worry if it fails for the first few minutes. Any weather API works the same way: the city and key go in the request, and JSON comes back. The code below shows that shape.
🔑 Keeping your API key safe
Before we write the real code, let’s talk about the key, because people get this wrong all the time. Your API key is like a password. If someone copies it, they can use the service as you, and on a paid plan that could cost you money. So you do NOT want the key sitting inside your code.
Here is the rule:
- Never type the key directly into your
.pyfile. If you do that and later share the file or push it to GitHub, your secret is out in the open for everyone. - Instead, store the key outside the code, in an environment variable. An environment variable is a named value that lives in your operating system, not in your file. Your program reads it at runtime.
- Then your code file has no secret in it at all. You can share it freely.
So how do you set an environment variable? You set it in your terminal before running the program. On macOS or Linux you write export WEATHER_API_KEY="your_real_key", and on Windows PowerShell you write $env:WEATHER_API_KEY="your_real_key". Then you run your script in that same terminal.
This tiny function reads the key back inside Python:
import os
api_key = os.environ.get("WEATHER_API_KEY")Let’s read it:
osis the built-in module that lets Python talk to the operating system.os.environis a dictionary-like object holding all the environment variables.os.environ.get("WEATHER_API_KEY")looks up our variable by name. If it is missing,.getreturnsNoneinstead of crashing, so we can check for that and show a friendly message.
The why is simple. Secrets and code live in different places. Your code says HOW to fetch weather. Your environment says WHO you are. Mixing them is how keys get leaked.
Caution
Do not hardcode the key like api_key = "abc123...". That feels easier, but the first time you push that file to a public repo, bots scan and steal the key within minutes. Read it from the environment every time.
📦 Step 1: Sending the request
We use the requests library to talk to the API. Install it once with pip install requests if you have not already. Then we build the web address with the city and key, send the request, and get a response back.
This function asks the weather service for one city’s data:
import requests
def get_weather(city, api_key): url = "https://api.openweathermap.org/data/2.5/weather" params = { "q": city, "appid": api_key, "units": "metric", } response = requests.get(url, params=params, timeout=10) return responseLet’s read it line by line:
urlis the base web address of the weather service. It never changes.paramsis a dictionary of the extra details.qis the city you are asking about.appidis your API key.unitsset to"metric"asks the service to give us Celsius and not the default.requests.get(url, params=params, timeout=10)actually sends the request. The library glues the url and params together into the full address for you, so you do not build messy strings by hand.timeout=10says “wait at most ten seconds for a reply”. Without it, if the server hangs, your program could wait forever. The timeout keeps it from getting stuck.- We return the whole
responseso the caller can check it.
So this function just sends the question and hands back the reply. The next step is reading that reply.
Tip
Always pass timeout= to requests.get. A real network can be slow or silent. The timeout means your app fails fast with a clear error instead of freezing.
📊 Step 2: Reading the JSON response
The service replies with JSON. We first check that the request actually worked, then pull out the pieces we want, like the temperature and the description.
This reads the response and prints the weather:
def show_weather(response): if response.status_code == 404: print("City not found. Check the spelling and try again.") return if response.status_code != 200: print("Could not get weather. Check your API key.") return
data = response.json() city = data["name"] temp = data["main"]["temp"] feels = data["main"]["feels_like"] description = data["weather"][0]["description"]
print(f"\nWeather in {city}:") print(f"Temperature: {temp} C (feels like {feels} C)") print(f"Condition: {description}")Here is what is going on:
response.status_codeis a number the server sends to say how it went.200means success.404means the city was not found. Other numbers mean other problems, like a401when your key is wrong.- We check
404first so we can give a clear “city not found” message. Then we catch any other non-200code with a general message. Only after both checks do we trust the data. response.json()turns the JSON text into a Python dictionary we can read.- Then we dig into that dictionary.
data["name"]is the city name the service confirms.data["main"]["temp"]reaches the temperature, anddata["main"]["feels_like"]reaches what it feels like with wind and humidity.data["weather"][0]["description"]reaches text like “clear sky”. Noticeweatheris a list, so we grab the first item with[0]before reading inside it.
Reading nested JSON like this is exactly what you practiced in the APIs module. The trick is to read the JSON one layer at a time: a key, then a key inside that, then maybe an index into a list.
Caution
Checking status_code first is important. If you skip it and the city was wrong, response.json() would not have the keys you expect. Reaching for data["main"] would then crash with a KeyError. Always confirm success before reading the data.
🌡️ A note on units and temperature
You might wonder why we bothered with "units": "metric" up in the params. Here is the reason. By default, OpenWeatherMap gives temperature in Kelvin, which is the scientific scale where water freezes at about 273. Nobody checks the weather in Kelvin, right?
You have two ways to fix that:
- The easy way, which we used: add
"units": "metric"to the params and the service sends Celsius directly. For Fahrenheit you would use"imperial"instead. - The manual way: if for some reason you get Kelvin, you convert it yourself. Celsius is just Kelvin minus 273.15.
This is the manual conversion, in case you ever receive Kelvin:
kelvin = 291.45celsius = kelvin - 273.15print(f"{celsius:.1f} C")A quick walkthrough:
kelvin - 273.15shifts the scale down to Celsius. That single subtraction is the whole formula.{celsius:.1f}rounds the number to one decimal place when printing, so you see18.3 Cand not a long ugly decimal. The.1fmeans “one digit after the point”.
So letting the API do the unit work with units=metric is cleaner. But it is good to know the formula, because not every API offers a units option.
🖥️ Step 3: The complete program
Here is the full weather app with a loop, so you can check city after city. Notice the key comes from the environment, never typed into the file. Save this as weather.py, set your WEATHER_API_KEY in the same terminal, and run it.
import osimport requests
def get_weather(city, api_key): url = "https://api.openweathermap.org/data/2.5/weather" params = {"q": city, "appid": api_key, "units": "metric"} return requests.get(url, params=params, timeout=10)
def show_weather(response): if response.status_code == 404: print("City not found. Check the spelling and try again.") return if response.status_code != 200: print("Could not get weather. Check your API key.") return
data = response.json() try: city = data["name"] temp = data["main"]["temp"] feels = data["main"]["feels_like"] description = data["weather"][0]["description"] except (KeyError, IndexError): print("The reply was missing some weather details.") return
print(f"\nWeather in {city}:") print(f"Temperature: {temp} C (feels like {feels} C)") print(f"Condition: {description}")
def main(): api_key = os.environ.get("WEATHER_API_KEY") if not api_key: print("No API key found. Set WEATHER_API_KEY first.") return
print("Weather App") while True: city = input("\nEnter a city (or 'q' to quit): ") if city == "q": print("Goodbye!") break try: response = get_weather(city, api_key) show_weather(response) except requests.Timeout: print("The service took too long. Try again.") except requests.RequestException: print("Network problem. Please check your internet.")
main()Let’s walk through the new safety parts, because this version handles real-world trouble:
api_key = os.environ.get("WEATHER_API_KEY")reads the key from the environment. If it is missing we print a message and stop, instead of sending a broken request.- Inside
show_weather, thetry/except (KeyError, IndexError)guards against a reply that is missing a field. Even after a200, an odd response could lack a key, and this catches that without crashing. - In
main, we catchrequests.Timeoutfor a slow server, and the broaderrequests.RequestExceptionfor any other network failure, like no internet at all.Timeoutis a kind ofRequestException, so we list it first to give it its own message.
So this one program now covers the four things that go wrong in real life: a missing key, a wrong city, a missing field, and a dead network. Each one gets a clear message, and the app keeps running.
Here is a sample run:
Output
Weather App
Enter a city (or 'q' to quit): London
Weather in London:Temperature: 18.3 C (feels like 17.9 C)Condition: light rain
Enter a city (or 'q' to quit): Lndon
City not found. Check the spelling and try again.
Enter a city (or 'q' to quit): qGoodbye!The exact numbers depend on the real weather when you run it, so yours will differ. But the shape is the same. You type a city, the app fetches live data from the internet, and shows it. Notice the second city was misspelled, and the app handled it cleanly instead of crashing.
🐢 Respect the rate limit
One more real-world habit. A free API key comes with a rate limit, which is a cap on how many requests you can send in a given time. OpenWeatherMap’s free tier allows roughly sixty calls a minute. That is plenty for a person typing cities by hand.
Where people get in trouble is loops:
- Do not put
requests.getinside a tight loop that fires hundreds of times with no pause. You will hit the limit and the service will start refusing you with a429status. - If you ever fetch many cities in a row, add a small pause between calls with
time.sleep(1). One second between requests is polite and keeps you under the cap. - Cache results you already have. If a user asks for London twice in a minute, you can reuse the first answer instead of asking again.
The why is fairness. The free service is shared by many people. Sending a flood of requests is rude to them and gets your key blocked. Asking only when you need to keeps everyone happy.
🚀 Extend it further
You have a working app. Now make it yours. Here are a few directions to take it:
- Add an emoji per condition. Map each weather description to an emoji, so “clear sky” shows ☀️ and “light rain” shows 🌧️. A small dictionary from keyword to emoji does the job.
- Show a 5-day forecast. OpenWeatherMap has a forecast endpoint at
.../data/2.5/forecast. It returns a list of future time slots. Loop over them and print a few, so the user sees what is coming. - Save searches to a file. Each time someone checks a city, append it to a text file with the date. Now you have a little history of what people looked up, using the file handling you already know.
None of these need new libraries. They build on requests, dictionaries, loops, and files, which you already have.
🛠️ Practice Challenge
Try this one before peeking. Add a feature to the app: print a short message based on the temperature, like “Bring an umbrella” when it is raining, or “It’s hot, stay hydrated” when the temperature is above 30 C. Write a small function advice(temp, description) that returns the right line, and call it inside show_weather.
⚠️ Common Mistakes
A few traps in this project:
- Hardcoding the API key. Typing the key into the file feels quick, but it leaks the moment you share or push the file. Read it from
os.environ.get("WEATHER_API_KEY")instead. - Not checking
status_codebefore reading the data. A wrong city returns an error response with no weather keys, so readingdata["main"]crashes. Check for200first, and handle404with its own message. - Forgetting the timeout. Without
timeout=, a silent server can freeze your whole program. Always set a timeout onrequests.get. - Ignoring network errors. The internet can fail. Wrapping the request in
try/except requests.RequestExceptionkeeps the app alive when the connection drops. - Spamming the API. Firing requests in a tight loop hits the rate limit and gets your key refused. Ask only when you need to, and pause between bulk calls.
✅ Best Practices
Habits this project teaches:
- Keep your API key in an environment variable, never in the code. Secrets and code belong in different places.
- Pass request details as a
paramsdictionary, not by gluing strings together. It is cleaner and the library handles the formatting. - Always set a
timeoutand always check the response status before trusting the data. - Handle network errors with
try/except, because anything over the internet can fail. - Stay under the rate limit. Pause between bulk requests and reuse answers you already have.
🧩 What You’ve Learned
✅ You built a weather app that fetches live data from an online API.
✅ An API key is a secret code that identifies you, and you keep it in an environment variable with os.environ.get, never in the code.
✅ requests.get(url, params=..., timeout=...) sends the request, and response.json() turns the reply into a Python dictionary.
✅ Checking response.status_code (200 for success, 404 for a missing city) confirms things worked before you read the data.
✅ Reading nested JSON, like data["main"]["temp"], pulls out exactly the values you want, and units=metric gives you Celsius.
✅ Real apps handle a wrong city, a missing field, a dead network, and rate limits, all with clear messages instead of crashes.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
Where should you keep your API key?
Why: An API key is a secret. Keeping it in an environment variable and reading it with os.environ.get keeps it out of your code, so it does not leak when you share the file.
- 2
What does response.json() do?
Why: The service replies with JSON text. response.json() converts it into a Python dictionary so you can pull out values like the temperature.
- 3
Why check response.status_code before reading the data?
Why: A status of 200 means the request worked. For an error like a wrong city (404), the reply lacks the expected keys, so reading it would raise a KeyError.
- 4
Why add timeout= to requests.get and catch requests.RequestException?
Why: A timeout stops a silent server from freezing your program, and catching the network error lets the app show a message and keep running instead of crashing.
🚀 What’s Next?
You connected your program to a live online service, a huge milestone. Next we build a to-do application, bringing together CRUD and file storage into a clean, everyday tool.