Python Weather Application

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 .py file. 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:

  • os is the built-in module that lets Python talk to the operating system.
  • os.environ is 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, .get returns None instead 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 response

Let’s read it line by line:

  • url is the base web address of the weather service. It never changes.
  • params is a dictionary of the extra details. q is the city you are asking about. appid is your API key. units set 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=10 says “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 response so 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_code is a number the server sends to say how it went. 200 means success. 404 means the city was not found. Other numbers mean other problems, like a 401 when your key is wrong.
  • We check 404 first so we can give a clear “city not found” message. Then we catch any other non-200 code 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, and data["main"]["feels_like"] reaches what it feels like with wind and humidity. data["weather"][0]["description"] reaches text like “clear sky”. Notice weather is 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.45
celsius = kelvin - 273.15
print(f"{celsius:.1f} C")

A quick walkthrough:

  • kelvin - 273.15 shifts 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 see 18.3 C and not a long ugly decimal. The .1f means “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 os
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"}
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, the try / except (KeyError, IndexError) guards against a reply that is missing a field. Even after a 200, an odd response could lack a key, and this catches that without crashing.
  • In main, we catch requests.Timeout for a slow server, and the broader requests.RequestException for any other network failure, like no internet at all. Timeout is a kind of RequestException, 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): q
Goodbye!

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.get inside a tight loop that fires hundreds of times with no pause. You will hit the limit and the service will start refusing you with a 429 status.
  • 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_code before reading the data. A wrong city returns an error response with no weather keys, so reading data["main"] crashes. Check for 200 first, and handle 404 with its own message.
  • Forgetting the timeout. Without timeout=, a silent server can freeze your whole program. Always set a timeout on requests.get.
  • Ignoring network errors. The internet can fail. Wrapping the request in try / except requests.RequestException keeps 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 params dictionary, not by gluing strings together. It is cleaner and the library handles the formatting.
  • Always set a timeout and 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. 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. 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. 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. 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.

To-Do Application

Share & Connect