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

  1. Go to https://www.python.org/downloads/windows/ and download the latest Python 3.12 or 3.13 installer (64-bit).
  2. Start it. On the first screen tick "Add python.exe to PATH". This is important.
  3. 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 py is not found, try python --version. If neither works, run the installer again, choose Modify, and make sure "Add Python to environment variables" is ticked.

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, use py.

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:

https://api.open-meteo.com/v1/forecast?latitude=47.4979&longitude=19.0402&daily=temperature_2m_max,temperature_2m_min&timezone=Europe/Budapest&forecast_days=3

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

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 start python again. 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")

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}")

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

Extra

  1. Print also the difference between the highest and the lowest forecast for each city.
  2. Open-Meteo can send rain too: add precipitation_sum to daily in fetch_open_meteo and 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.

← OverviewWeek 2 →