Week 1: Installation and getting data from the internet
Goal of today: a working Python setup, and a program that asks three weather services for tomorrow's temperature in Budapest and Eger:
Forecasts for tomorrow (2026-10-07)
Budapest
open_meteo max 25.0 °C min 12.3 °C
met_norway max 22.8 °C min 9.0 °C
wttr max 25.0 °C min 14.0 °C
Eger
open_meteo max 24.4 °C min 11.9 °C
met_norway max 22.0 °C min 10.4 °C
wttr max 23.0 °C min 10.0 °C
The three services disagree by up to 3 °C. Which one should we believe? That question is the whole project.
How to work: do the steps in order. After each step, run the code and compare with the expected output. Go on only when yours looks the same. (Your temperatures will differ, because you run it on another day.)
What you need to know
You need only basic Python. Here is everything this week uses, in short.
Terminal. A window where you type commands instead of clicking. On Windows: Command Prompt (cmd) or
PowerShell. cd C:\weather-lab goes into a folder; python file.py runs a Python file.
Variables, lists, dictionaries.
city = "Eger" # text (string)
temps = [23.0, 25.0, 24.1] # list: values in order, positions start at 0
temps[1] # -> 25.0 (the second element)
eger = {"lat": 47.9025, "lon": 20.3772} # dictionary: name -> value
eger["lat"] # -> 47.9025
Tuple. Like a list, but fixed: (25.0, 12.3). We use it for "(max, min)". You can unpack it: tmax, tmin = (25.0, 12.3).
Functions. A named piece of code with inputs (parameters) and an output (return):
def add_one(x):
return x + 1
add_one(4) # -> 5
Loops. for repeats something for every element:
for name, data in {"Budapest": 1, "Eger": 2}.items():
print(name, data)
Errors: try / except. If something inside try fails, Python jumps to except instead of stopping the program:
try:
x = 1 / 0
except Exception as error:
print("FAILED:", error) # -> FAILED: division by zero
Modules and packages. A .py file is a module; from config import CITIES takes CITIES out of config.py.
A package is code written by others (e.g. requests); you install it with pip and use it with import.
f-strings. f"max {tmax} °C" puts the value of tmax into the text. {tmax:5.1f} means: 5 characters wide, 1 decimal.
Part 1: Installation
You do this once. It takes about 20 minutes.
Step 1.1: Install Python
- Go to https://www.python.org/downloads/windows/ and download the latest Python 3.12 or 3.13 installer (64-bit).
- Start it. On the first screen tick "Add python.exe to PATH". This is important.
- Click Install Now.
Check it: open a new Command Prompt (Start menu → type cmd → Enter) and type:
py --version
You should see something like Python 3.12.7.
If
pyis not found, trypython --version. If neither works, run the installer again, choose Modify, and make sure "Add Python to environment variables" is ticked.
Step 1.2: Install an editor (recommended)
Install Visual Studio Code (https://code.visualstudio.com) and, inside it, the Python extension (Extensions icon on the left → search "Python" → Install, the one by Microsoft).
Step 1.3: Create the project folder
We use C:\weather-lab in this guide. You can use another folder, but avoid spaces and accents in the path
(e.g. not C:\Users\Kovács Béla\Asztal\...). Some tools have problems with them.
mkdir C:\weather-lab
cd C:\weather-lab
In VS Code: File → Open Folder → C:\weather-lab. Open a terminal inside VS Code with Terminal → New Terminal.
Step 1.4: Create a virtual environment
A virtual environment (venv) is a private copy of Python just for this project.
Packages you install go into it, not into the whole computer. If something goes wrong, you can delete the
.venv folder and create it again.
py -m venv .venv
This creates a folder called .venv. Now activate it.
In Command Prompt (cmd):
.venv\Scripts\activate.bat
In PowerShell (the default terminal in VS Code):
.venv\Scripts\Activate.ps1
PowerShell error: "running scripts is disabled on this system" Run this once, then try again:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
When it works, the prompt starts with (.venv):
(.venv) C:\weather-lab>
Every time you open a new terminal you must activate the venv again.
(VS Code usually does it for you once it has noticed the .venv folder.
If it asks "select interpreter", choose the one in .venv.)
Step 1.5: Install the packages
Create a file called requirements.txt in C:\weather-lab with this content:
requests
pandas
scikit-learn
flask
tzdata
pytest
| Package | What it is for |
|---|---|
requests |
downloading data from web APIs |
pandas |
tables of data (like Excel in Python) |
scikit-learn |
machine learning: regressors, neural net |
flask |
the web application |
tzdata |
time zone information (Windows does not have it built in for Python) |
pytest |
automatic tests (week 8) |
Install them (with the venv active!):
pip install -r requirements.txt
Check:
python -c "import requests, pandas, sklearn, flask; print('all good')"
Inside the venv you can simply write
python; outside it, usepy.
Step 1.6: Optional: a database viewer
DB Browser for SQLite (https://sqlitebrowser.org) lets you open weather.db and look at the tables.
Very useful from week 2. Close it before your scripts write to the database.
Linux or macOS
Everything is the same, except:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Instead of Windows Task Scheduler (week 2) you use cron.
Part 2: Getting data from the internet
Work in C:\weather-lab with the venv active ((.venv) at the start of the line).
Step 2.1: Look at an API in the browser
An API is a web address that answers programs instead of people. Open this address in your browser:
You should see text like this:
{"latitude":47.501236,"longitude":19.03534, ... "daily":{"time":["2026-10-06","2026-10-07","2026-10-08"],
"temperature_2m_max":[24.3,25.0,24.1],"temperature_2m_min":[13.0,12.3,14.5]}}
This format is called JSON. It looks like Python: {...} is a dictionary, [...] is a list.
Look at the address: everything after ? are parameters: where (latitude, longitude), what (daily=...),
in which time zone, for how many days. time[1] is tomorrow, and temperature_2m_max[1] is tomorrow's maximum.
Step 2.2: The same from Python
Create a file try_api.py:
import requests
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": 47.4979,
"longitude": 19.0402,
"daily": "temperature_2m_max,temperature_2m_min",
"timezone": "Europe/Budapest",
"forecast_days": 3,
}
response = requests.get(url, params=params, timeout=30)
print(response.status_code)
print(response.json())
Run it:
python try_api.py
You should see:
200
{'latitude': 47.501236, 'longitude': 19.03534, ..., 'daily': {'time': ['2026-10-06', '2026-10-07', '2026-10-08'], 'temperature_2m_max': [24.3, 25.0, 24.1], 'temperature_2m_min': [13.0, 12.3, 14.5]}}
What happened:
- requests.get() sends the request. You give the parameters as a dictionary; requests builds the address for you.
- timeout=30: wait at most 30 seconds for an answer.
- 200 is the status code: it means OK. (404 = not found, 403 = forbidden, 500 = server error.)
- .json() turns the JSON text into a Python dictionary.
Step 2.3: Take out only what we need
Replace the two print lines at the end of try_api.py with:
daily = response.json()["daily"]
print(daily["time"])
print(daily["temperature_2m_max"])
You should see:
['2026-10-06', '2026-10-07', '2026-10-08']
[24.3, 25.0, 24.1]
Two lists of the same length. Same position = same day.
Step 2.4: Find tomorrow automatically
We do not want to type tomorrow's date by hand. Change try_api.py to this:
from datetime import date, timedelta
import requests
tomorrow = (date.today() + timedelta(days=1)).isoformat()
print("Tomorrow is", tomorrow)
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": 47.4979,
"longitude": 19.0402,
"daily": "temperature_2m_max,temperature_2m_min",
"timezone": "Europe/Budapest",
"forecast_days": 3,
}
response = requests.get(url, params=params, timeout=30)
daily = response.json()["daily"]
i = daily["time"].index(tomorrow)
print("Position in the list:", i)
print("Max:", daily["temperature_2m_max"][i], "Min:", daily["temperature_2m_min"][i])
You should see:
Tomorrow is 2026-10-07
Position in the list: 1
Max: 25.0 Min: 12.3
date.today() + timedelta(days=1)is tomorrow;.isoformat()writes it as text"2026-10-07", the same format as the API..index(tomorrow)finds where tomorrow is in the list. We use that position in the other lists.
try_api.py was only for trying. From now on we write the real program in separate files.
Step 2.5: config.py: the settings in one place
Create config.py:
config.py
"""Settings shared by all scripts."""
TIMEZONE = "Europe/Budapest"
CITIES = {
"budapest": {"name": "Budapest", "lat": 47.4979, "lon": 19.0402},
"eger": {"name": "Eger", "lat": 47.9025, "lon": 20.3772},
}
# MET Norway wants to know who is asking. You may add your own e-mail address,
# but NOT a fake one like ...@example.com (that is blocked with error 403).
USER_AGENT = "weather-lab/1.0 (university lab project)"
Cities, time zone and other settings live here. If you want a third city later, you change only this file.
(USER_AGENT is needed in step 2.9.)
Step 2.6: sources.py: the first function
Create sources.py. For now it has only the Open-Meteo part. It is the code of step 2.4, turned into functions:
"""Download forecasts from the internet."""
from datetime import date, timedelta
import requests
from config import TIMEZONE
TIMEOUT = 30 # seconds to wait for an answer
def tomorrow():
return (date.today() + timedelta(days=1)).isoformat()
def fetch_open_meteo(city):
params = {
"latitude": city["lat"], "longitude": city["lon"],
"daily": "temperature_2m_max,temperature_2m_min",
"timezone": TIMEZONE, "forecast_days": 3,
}
response = requests.get("https://api.open-meteo.com/v1/forecast", params=params, timeout=TIMEOUT)
response.raise_for_status()
daily = response.json()["daily"]
i = daily["time"].index(tomorrow())
return daily["temperature_2m_max"][i], daily["temperature_2m_min"][i]
New things compared with step 2.4:
- The city is a parameter, so the same function works for Budapest and Eger.
- response.raise_for_status() stops with an error if the status code is not OK.
Without it, an error page could be used as if it was weather data.
- The function returns (max, min) instead of printing it. Whoever calls it decides what to do with it.
Try it in the interactive Python (type the lines after >>>):
python
>>> from config import CITIES
>>> from sources import fetch_open_meteo
>>> fetch_open_meteo(CITIES["budapest"])
(25.0, 12.3)
>>> fetch_open_meteo(CITIES["eger"])
(24.4, 11.9)
>>> exit()
Important: after you change
sources.py,exit()and startpythonagain. The interactive Python does not notice changes in files it has already imported.
Step 2.7: The second service: wttr.in
Look at it in the browser first: https://wttr.in/47.9025,20.3772?format=j1
It is a long JSON. The interesting part is the list weather, one element per day:
"weather": [ {"date": "2026-10-06", "maxtempC": "23", "mintempC": "11", ...},
{"date": "2026-10-07", "maxtempC": "23", "mintempC": "10", ...}, ... ]
Notice: the temperatures are text ("23"), not numbers.
Add this function to the end of sources.py:
def fetch_wttr(city):
url = f"https://wttr.in/{city['lat']},{city['lon']}"
response = requests.get(url, params={"format": "j1"}, timeout=TIMEOUT)
response.raise_for_status()
for day in response.json()["weather"]:
if day["date"] == tomorrow():
return float(day["maxtempC"]), float(day["mintempC"])
raise ValueError("wttr.in did not send tomorrow's forecast")
- We go through the days and return the one whose date is tomorrow.
float("23")turns the text into the number23.0.- If tomorrow is not in the answer, we raise an error with a clear message, instead of returning nothing.
Try it:
python
>>> from config import CITIES
>>> from sources import fetch_wttr
>>> fetch_wttr(CITIES["eger"])
(23.0, 10.0)
>>> exit()
Step 2.8: Hourly values → daily max and min
The third service, MET Norway, does not give daily max/min. It gives a temperature for every hour. So first we write a function that turns hourly values into daily max/min.
Add to sources.py (before fetch_open_meteo):
def daily_max_min(times, temps):
"""Turn hourly temperatures into {day: (tmax, tmin)}.
times are local times like "2026-10-07T13:00". Days with fewer than
20 hourly values are skipped, because their max/min would be unreliable.
"""
by_day = {}
for time, temp in zip(times, temps):
if temp is not None:
by_day.setdefault(time[:10], []).append(temp)
return {day: (max(values), min(values)) for day, values in by_day.items() if len(values) >= 20}
How it works:
- time[:10] is the first 10 characters of "2026-10-07T13:00", that is, the day "2026-10-07".
- by_day collects the values of each day into a list: {"2026-10-07": [10, 9, 9, ...], ...}.
- Then we take max and min of each list.
- Days with fewer than 20 hourly values are dropped. The max of only a few hours is not a real daily max.
Try it with made-up data: a whole day (24 hours) and two hours of the next day:
python
>>> from sources import daily_max_min
>>> times = [f"2026-10-07T{h:02d}:00" for h in range(24)] + ["2026-10-08T00:00", "2026-10-08T06:00"]
>>> temps = [10, 9, 9, 8, 8, 8, 9, 11, 14, 17, 19, 21, 22, 23, 23, 22, 21, 19, 16, 14, 13, 12, 11, 11, 10, 9]
>>> daily_max_min(times, temps)
{'2026-10-07': (23, 8)}
>>> exit()
The 7th has max 23 and min 8. The 8th is dropped: it has only 2 values.
Step 2.9: The third service: MET Norway
Look at the raw answer first. In the interactive Python:
python
>>> import requests
>>> r = requests.get("https://api.met.no/weatherapi/locationforecast/2.0/compact",
... params={"lat": 47.9025, "lon": 20.3772},
... headers={"User-Agent": "weather-lab/1.0 (university lab project)"})
>>> steps = r.json()["properties"]["timeseries"]
>>> for s in steps[:3]:
... print(s["time"], s["data"]["instant"]["details"]["air_temperature"])
...
2026-10-06T12:00:00Z 22.4
2026-10-06T13:00:00Z 22.5
2026-10-06T14:00:00Z 22.1
>>> exit()
Two things to notice:
1. MET Norway wants to know who is asking: the User-Agent header. Without it, the answer is 403 Forbidden.
2. The times end with Z, which means UTC (London winter time). Hungary is 1 or 2 hours ahead.
2026-10-06T22:00Z is already midnight, 7 October, in Hungary. If we do not convert, the night hours count for the wrong day.
Converting a time looks like this:
python
>>> from datetime import datetime
>>> from zoneinfo import ZoneInfo
>>> utc_time = datetime.fromisoformat("2026-10-06T22:00:00Z".replace("Z", "+00:00"))
>>> utc_time.astimezone(ZoneInfo("Europe/Budapest"))
datetime.datetime(2026, 10, 7, 0, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Budapest'))
>>> exit()
Now put it together. First change the imports at the top of sources.py to:
from datetime import date, datetime, timedelta
from zoneinfo import ZoneInfo
import requests
from config import TIMEZONE, USER_AGENT
TIMEOUT = 30 # seconds to wait for an answer
LOCAL = ZoneInfo(TIMEZONE)
Then add this function after fetch_open_meteo:
def fetch_met_norway(city):
params = {"lat": round(city["lat"], 4), "lon": round(city["lon"], 4)}
response = requests.get("https://api.met.no/weatherapi/locationforecast/2.0/compact",
params=params, headers={"User-Agent": USER_AGENT}, timeout=TIMEOUT)
response.raise_for_status()
times, temps = [], []
for step in response.json()["properties"]["timeseries"]:
# MET Norway gives UTC times ("...Z"); convert them to Hungarian time.
utc_time = datetime.fromisoformat(step["time"].replace("Z", "+00:00"))
times.append(utc_time.astimezone(LOCAL).strftime("%Y-%m-%dT%H:%M"))
temps.append(step["data"]["instant"]["details"]["air_temperature"])
return daily_max_min(times, temps)[tomorrow()]
It collects the times (converted to Hungarian time) and temperatures into two lists, gives them to daily_max_min(), and returns tomorrow.
Try it:
python
>>> from config import CITIES
>>> from sources import fetch_met_norway
>>> fetch_met_norway(CITIES["eger"])
(22.0, 10.4)
>>> exit()
Step 2.10: All services in one dictionary
Add this at the end of sources.py:
PROVIDERS = {"open_meteo": fetch_open_meteo, "met_norway": fetch_met_norway, "wttr": fetch_wttr}
It is a dictionary: name → function (without (), so the function is not called here, only stored).
Later we can go through all services with one loop, and adding a fourth service will mean adding one line here.
Step 2.11: The program
Create first_look.py:
first_look.py
"""Week 1: ask the three providers for tomorrow's forecast and print it.
python first_look.py
"""
from config import CITIES
from sources import PROVIDERS, tomorrow
print(f"Forecasts for tomorrow ({tomorrow()})\n")
for city in CITIES.values():
print(city["name"])
for source, fetch in PROVIDERS.items():
try:
tmax, tmin = fetch(city)
print(f" {source:11s} max {tmax:5.1f} °C min {tmin:5.1f} °C")
except Exception as error:
print(f" {source:11s} FAILED: {error}")
- The outer loop goes through the cities, the inner loop through the services.
fetch(city)calls the function stored in the dictionary.try / except: if one service is down, we printFAILEDand continue with the others.{tmax:5.1f}prints the number 5 characters wide with 1 decimal, so the columns line up.
Run it:
python first_look.py
You should see the output from the top of this page. Done!
Your complete sources.py
Compare with yours. If something does not work, copy this one.
sources.py
"""Download forecasts from the internet.
Every function returns plain Python values: (tmax, tmin) or {day: (tmax, tmin)}.
A day is always a text like "2026-10-07" in local (Hungarian) time.
"""
from datetime import date, datetime, timedelta
from zoneinfo import ZoneInfo
import requests
from config import TIMEZONE, USER_AGENT
TIMEOUT = 30 # seconds to wait for an answer
LOCAL = ZoneInfo(TIMEZONE)
def tomorrow():
return (date.today() + timedelta(days=1)).isoformat()
def daily_max_min(times, temps):
"""Turn hourly temperatures into {day: (tmax, tmin)}.
times are local times like "2026-10-07T13:00". Days with fewer than
20 hourly values are skipped, because their max/min would be unreliable.
"""
by_day = {}
for time, temp in zip(times, temps):
if temp is not None:
by_day.setdefault(time[:10], []).append(temp)
return {day: (max(values), min(values)) for day, values in by_day.items() if len(values) >= 20}
# ---------- Live set: three providers, tomorrow only ----------
def fetch_open_meteo(city):
params = {
"latitude": city["lat"], "longitude": city["lon"],
"daily": "temperature_2m_max,temperature_2m_min",
"timezone": TIMEZONE, "forecast_days": 3,
}
response = requests.get("https://api.open-meteo.com/v1/forecast", params=params, timeout=TIMEOUT)
response.raise_for_status()
daily = response.json()["daily"]
i = daily["time"].index(tomorrow())
return daily["temperature_2m_max"][i], daily["temperature_2m_min"][i]
def fetch_met_norway(city):
params = {"lat": round(city["lat"], 4), "lon": round(city["lon"], 4)}
response = requests.get("https://api.met.no/weatherapi/locationforecast/2.0/compact",
params=params, headers={"User-Agent": USER_AGENT}, timeout=TIMEOUT)
response.raise_for_status()
times, temps = [], []
for step in response.json()["properties"]["timeseries"]:
# MET Norway gives UTC times ("...Z"); convert them to Hungarian time.
utc_time = datetime.fromisoformat(step["time"].replace("Z", "+00:00"))
times.append(utc_time.astimezone(LOCAL).strftime("%Y-%m-%dT%H:%M"))
temps.append(step["data"]["instant"]["details"]["air_temperature"])
return daily_max_min(times, temps)[tomorrow()]
def fetch_wttr(city):
url = f"https://wttr.in/{city['lat']},{city['lon']}"
response = requests.get(url, params={"format": "j1"}, timeout=TIMEOUT)
response.raise_for_status()
for day in response.json()["weather"]:
if day["date"] == tomorrow():
return float(day["maxtempC"]), float(day["mintempC"])
raise ValueError("wttr.in did not send tomorrow's forecast")
PROVIDERS = {"open_meteo": fetch_open_meteo, "met_norway": fetch_met_norway, "wttr": fetch_wttr}
You can delete try_api.py now.
Check
- ☐
python first_look.pyprints 6 lines with realistic temperatures. - ☐ Add a third city to
config.py(Debrecen: 47.5316, 21.6273), run again. Then remove it. - ☐ Switch off the Wi-Fi and run: you see
FAILEDlines, but the program does not crash.
Extra
- Print also the difference between the highest and the lowest forecast for each city.
- Open-Meteo can send rain too: add
precipitation_sumtodailyinfetch_open_meteoand return it as a third value.
If something goes wrong
| Error | Reason |
|---|---|
ModuleNotFoundError: No module named 'requests' |
The venv is not active: run .venv\Scripts\activate. |
403 Client Error from met.no |
USER_AGENT is missing, or contains a fake e-mail like @example.com. |
ZoneInfoNotFoundError |
pip install tzdata |
ValueError: '2026-10-07' is not in list |
The service did not send tomorrow. Run again later. |
| A function "does not exist" in the interactive Python | You changed the file: exit() and start python again. |
ReadTimeout |
The service is slow. Run again. |
'py' is not recognized |
Python is not on PATH. Reinstall with "Add python.exe to PATH" ticked. |
No module named 'flask' (or similar) |
The venv is not active. Look for (.venv) in the prompt. |
pip install fails with SSL or proxy errors on the university network |
Try another network (e.g. phone hotspot) or ask the instructor for the proxy setting. |
| Python opens the Microsoft Store | Settings → Apps → Advanced app settings → App execution aliases → switch off python.exe and python3.exe. |