DEV Community

Cover image for Comparing weather across cities in Python: the timezone trap
Petrichor
Petrichor

Posted on

Comparing weather across cities in Python: the timezone trap

Charting one city's forecast is a five-minute job. Charting four on the same axis raises a question single-city tutorials never ask: what does "the same time" mean across timezones?

This is a ~50-line Streamlit dashboard built on Meteosource's weather API, using its /point endpoint. Free tier, 400 calls a day, no card.

pip install streamlit pandas requests
Enter fullscreen mode Exit fullscreen mode

The trap

Pull hourly forecasts for Prague and Reykjavík in local time, line them up in pandas, and Prague's 14:00 sits directly above Reykjavík's 14:00. Two moments two hours apart, drawn as though simultaneous. Every comparison you make from that chart is wrong.

Two defensible answers, and you have to pick one on purpose:

  • Absolute time. Request everything in UTC. "Right now, Barcelona is 17° warmer than Reykjavík." Correct for anything synchronised: flights, grid load, a live ops dashboard.
  • Local time. Request each city in its own zone, index by hour-of-day. "Barcelona's afternoon peak is 17° above Reykjavík's." Correct for comparing daily rhythms.

This one uses UTC, and you have to ask for it. Leave timezone out and the API gives you each point's own local time, which means the default is the trap. That's also why Meteosource's own Python wrapper, pymeteosource, always requests UTC internally and converts afterwards: same problem, same conclusion.

The app

import streamlit as st
import pandas as pd
import requests

API_KEY = st.secrets["METEOSOURCE_KEY"]
BASE_URL = "https://www.meteosource.com/api/v1/free/point"

CITIES = {
    "Prague": (50.0755, 14.4378),
    "Barcelona": (41.3874, 2.1686),
    "Reykjavík": (64.1466, -21.9426),
    "Athens": (37.9838, 23.7275),
}

st.title("🌍 City temperature comparison")

chosen = st.multiselect("Cities", list(CITIES),
                        default=["Prague", "Barcelona", "Reykjavík"])


@st.cache_data(ttl=1800)
def fetch(city):
    lat, lon = CITIES[city]
    # timezone=UTC is not the default, so it has to be explicit
    params = {"lat": lat, "lon": lon, "sections": "hourly",
              "timezone": "UTC", "units": "metric", "key": API_KEY}
    response = requests.get(BASE_URL, params=params, timeout=10)
    response.raise_for_status()
    rows = response.json().get("hourly", {}).get("data", [])
    if not rows:
        return None
    df = pd.DataFrame(rows)[["date", "temperature"]]
    df["date"] = pd.to_datetime(df["date"])
    df = df.set_index("date")
    return df.rename(columns={"temperature": city})


if chosen and st.button("Compare"):
    frames = []
    for city in chosen:
        try:
            frame = fetch(city)
        except requests.exceptions.RequestException as exc:
            st.error(f"{city}: {exc}")
            continue
        if frame is None:
            st.warning(f"{city}: no hourly data returned")
            continue
        frames.append(frame)

    if not frames:
        st.stop()

    wide = pd.concat(frames, axis=1)

    cols = st.columns(len(wide.columns))
    for col, city in zip(cols, wide.columns):
        col.metric(city, f"{wide[city].max():.0f} °C",
                   f"{wide[city].min():.0f} °C low", delta_color="off")

    st.line_chart(wide)
    st.caption("Hourly temperature in °C. All timestamps UTC.")
Enter fullscreen mode Exit fullscreen mode

Put your key in .streamlit/secrets.toml as METEOSOURCE_KEY = "...", then streamlit run app.py.

Streamlit dashboard titled City temperature comparison, with Prague, Barcelona and Reykjavík selected. Three metric tiles show highs of 27, 30 and 13 degrees with corresponding lows, above a chart of three hourly temperature curves

Layout illustration with sample values. The free tier returns one day of hourly forecast, so your own chart will be shorter.

Two things worth noting

The endpoint takes one point at a time, so fetch() returns a one-column frame named after the city and pd.concat(frames, axis=1) joins them on the shared UTC index. Add a tenth city and nothing else changes.

@st.cache_data(ttl=1800) matters more than it looks. Streamlit reruns the whole script on every widget interaction, so without it, ticking a checkbox costs one request per selected city and a few minutes of fiddling eats the daily quota.

Where to go next

  • Precipitation lives at precipitation.total, nested, so it needs pd.json_normalize.
  • Local-time alignment: pass each city its own timezone and index on hour-of-day. Watch out for the day DST ends, when local time repeats an hour and you get two records with the same timestamp. Drop the duplicate with df[~df.index.duplicated(keep="first")] or pd.concat will fail.
  • place_id instead of coordinates: resolve names via /find_places_prefix. For mountains it gets you the peak's real elevation, where raw coordinates can land you lower down.
  • Don't index by position. The docs are explicit that sections and variables can grow over time.

If your cities seem to disagree about when the sun comes up, it's the timezone. It's always the timezone.

Top comments (0)