Turn an hourly forecast into three clothing recommendations a day
Every weather app tells you it will be 9 degrees. Almost none tell you what that means for your coat. This tutorial turns a weather API forecast into clothing advice, three times a day, in about a hundred lines of Python.
You get a recommendation for morning, midday and evening, based on a real hourly forecast for London. It runs on Python and Streamlit, and it fits comfortably inside the 10-day free trial of the Meteosource Standard plan.
One API call gets you the numbers. Fifty lines of opinion turn them into "wear a hood, not an umbrella."
What you'll need
- Python 3.10 or later
- A Meteosource account on the Standard plan. The 10-day trial is free and needs a card; cancel inside the ten days and you are not charged. It includes 20,000 calls a day, and this dashboard uses a couple of dozen.
- Fifteen minutes
Create a virtual environment and install what we need:
python3 -m venv venv # Windows: python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install streamlit "pymeteosource[pandas]"
We are using the official Python wrapper, pymeteosource, rather than calling the endpoint directly. It handles timezone conversion and exports straight to pandas, which saves us most of the plumbing.
Step 1: Pick the right variables
The obvious variable is temperature. It is also the wrong one.
What you feel standing at a bus stop is feels_like, which folds in wind and humidity. Ten degrees in still air and ten degrees in a 40 mph gust are different clothing problems, and only one of those two numbers knows it.
Three variables do most of the work. feels_like sets the base layer, wind.gusts decides whether a knit jumper is enough, and precipitation.total and probability.precipitation between them separate "it is raining" from "it might rain, take the small umbrella." We also use uv_index and humidity for two smaller rules later on.
Here is a trimmed single hour of the forecast, showing only the fields we use:
{
"date": "2026-08-24T08:00:00",
"feels_like": 15.8,
"wind": { "speed": 2.9, "gusts": 3.7 },
"uv_index": 0.4,
"precipitation": { "total": 0 },
"probability": { "precipitation": 0 },
"humidity": 62
}
Two notes on what those numbers mean. precipitation.total is rain accumulated over that hour in millimetres, not an instantaneous rate. And we will request the uk unit system, which gives temperatures in Celsius and wind in mph. That is how a British reader thinks about wind, and getting the unit system wrong is the fastest way to build a dashboard that confidently gives terrible advice.
Step 2: Write down your opinions
Put the rules in their own file. This is the part you will actually tune, and it should not be tangled up with API calls or layout.
Create wardrobe.py:
"""Turn a weather slot into clothing advice.
Thresholds assume the `uk` unit system: Celsius, mph, mm."""
# feels_like (C) -> base layer. Upper bound of each band, exclusive.
LAYERS = [
(-5, "Thermals, heavy winter coat, hat and gloves. Cover your face."),
(2, "Winter coat, hat, gloves, scarf."),
(8, "Warm coat and a scarf. Long sleeves underneath."),
(14, "Light jacket or fleece over a long-sleeve top."),
(19, "Long sleeves, or a t-shirt with something to put on later."),
(25, "T-shirt weather."),
(999, "Lightest, loosest thing you own."),
]
GUSTS_BREEZY = 20 # mph, noticeable
GUSTS_WINDPROOF = 30 # mph, a knit layer stops working
GUSTS_NO_UMBRELLA = 40 # mph, umbrellas turn inside out
RAIN_LIGHT = 0.2 # mm in the hour, drizzle
RAIN_HEAVY = 2.0 # mm in the hour, properly wet
PROB_UMBRELLA = 30 # %, worth carrying one just in case
UV_SUNGLASSES = 3 # UV index at which sun becomes a factor
MUGGY_HUMIDITY = 75 # %, combined with warmth
def base_layer(feels_like):
for ceiling, advice in LAYERS:
if feels_like < ceiling:
return advice
return LAYERS[-1][1]
def recommend(feels_like, gusts, rain_rate, rain_chance,
uv_index, humidity):
"""Return (headline, notes) for one time slot.
Wind and rain can only push the recommendation upward. Being
slightly too warm is recoverable. Being cold and wet is not.
"""
notes = []
if gusts >= GUSTS_WINDPROOF:
# feels_like already accounts for wind, but it uses mean wind
# speed, so a gusty day is colder than it claims. We push it
# down once more, deliberately.
headline = base_layer(feels_like - 2)
notes.append(f"Gusts to {gusts:.0f} mph. Windproof outer layer, "
"not a knit.")
elif gusts >= GUSTS_BREEZY:
headline = base_layer(feels_like)
notes.append(f"Breezy, {gusts:.0f} mph gusts. Something that "
"closes at the front.")
else:
headline = base_layer(feels_like)
if rain_rate >= RAIN_HEAVY:
notes.append("Heavy rain. Waterproof, and shoes you don't mind "
"ruining.")
elif rain_rate >= RAIN_LIGHT:
notes.append("Drizzle. A shower-resistant layer is enough.")
elif rain_chance >= PROB_UMBRELLA:
notes.append(f"{rain_chance:.0f}% chance of rain. Take the small "
"umbrella.")
if rain_rate >= RAIN_LIGHT and gusts >= GUSTS_NO_UMBRELLA:
notes.append("Too windy for an umbrella. Wear a hood.")
if uv_index >= UV_SUNGLASSES:
notes.append(f"UV index {uv_index:.0f}. Sunglasses, and sunscreen if "
"you are out for more than half an hour.")
if humidity >= MUGGY_HUMIDITY and feels_like >= 22:
notes.append("Muggy. Natural fibres will be kinder than synthetics.")
return headline, notes
Two decisions in there are worth spelling out, because they are the difference between advice and noise.
Wind and rain only ever push upward. The errors are not symmetric. Being slightly overdressed is an annoyance you can fix by taking something off. Being underdressed in cold rain is a ruined afternoon.
Rain plus real wind means a hood, not an umbrella. This is the rule that makes people trust the thing. Anyone who has fought an umbrella down a windy street already knows it, and seeing a dashboard know it too is what makes it feel like it was built by someone who goes outside.
Step 3: Split the day into three
Now the app. Create app.py:
import os
from datetime import datetime
from zoneinfo import ZoneInfo
import streamlit as st
from pymeteosource.api import Meteosource
from pymeteosource.types import tiers, sections, units
from wardrobe import recommend
API_KEY = os.environ.get("METEOSOURCE_API_KEY", "YOUR-API-KEY")
TIER = tiers.STANDARD
PLACE_ID = "london"
TIMEZONE = "Europe/London"
# Each slot is a window, not a single hour. It rains at 08:30 and not
# at 07:00, and you still get wet.
SLOTS = [
("Morning", range(7, 10)),
("Midday", range(12, 15)),
("Evening", range(18, 21)),
]
@st.cache_data(ttl=1800)
def load_forecast():
"""Fetch the hourly forecast once. Cached, so a refresh is free.
We slice one response into three slots rather than making a
request per slot.
"""
ms = Meteosource(API_KEY, TIER)
forecast = ms.get_point_forecast(
place_id=PLACE_ID,
sections=[sections.HOURLY],
tz=TIMEZONE, # the library defaults to UTC, so be explicit
units=units.UK, # Celsius, but wind in mph
)
return forecast.hourly.to_pandas()
def summarise(window):
"""Collapse a window of hours into one worst case.
Coldest hour, strongest gust, wettest hour. Dressing for the
average of a window is how you end up cold in it.
"""
return dict(
feels_like=window["feels_like"].min(),
gusts=window["wind_gusts"].max(),
rain_rate=window["precipitation_total"].max(),
rain_chance=window["probability_precipitation"].max(),
uv_index=window["uv_index"].max(),
humidity=window["humidity"].max(),
)
st.set_page_config(page_title="What to wear", layout="centered")
st.title("What to wear in London")
# Today according to the forecast location, not according to your laptop.
today_local = datetime.now(ZoneInfo(TIMEZONE)).date()
st.caption(f"{today_local:%A %d %B}")
try:
df = load_forecast()
except Exception as exc:
st.error(f"Could not load the forecast: {exc}")
st.stop()
today = df[df.index.date == today_local]
if today.empty:
st.warning("No hours left for today. Come back tomorrow morning.")
st.stop()
for label, hours in SLOTS:
window = today[today.index.hour.isin(hours)]
if window.empty:
continue
conditions = summarise(window)
headline, notes = recommend(**conditions)
st.subheader(label)
st.markdown(f"**{headline}**")
for note in notes:
st.markdown(f"- {note}")
st.caption(
f"Feels like {conditions['feels_like']:.0f}°C, "
f"gusts {conditions['gusts']:.0f} mph, "
f"{conditions['rain_chance']:.0f}% chance of rain"
)
with st.expander("The whole day, hour by hour"):
st.line_chart(today[["temperature", "feels_like"]])
st.dataframe(today[["feels_like", "wind_gusts", "precipitation_total",
"probability_precipitation", "uv_index"]])
One thing to watch: pymeteosource returns UTC by default, so set tz to your own timezone or the slots drift by an hour or more. Work out "today" in that same timezone and it stays right wherever you run it.
Step 4: Run it
export METEOSOURCE_API_KEY='your-actual-key'
streamlit run app.py
On a typical London day you get something like this:
Morning — Warm coat and a scarf. Long sleeves underneath.
- Drizzle. A shower-resistant layer is enough.
- Feels like 6°C, gusts 18 mph, 75% chance of rain
Midday — Light jacket or fleece over a long-sleeve top.
- Breezy, 24 mph gusts. Something that closes at the front.
- Feels like 12°C, gusts 24 mph, 20% chance of rain
Evening — Warm coat and a scarf. Long sleeves underneath.
- Gusts to 41 mph. Windproof outer layer, not a knit.
- Heavy rain. Waterproof, and shoes you don't mind ruining.
- Too windy for an umbrella. Wear a hood.
- Feels like 8°C, gusts 41 mph, 95% chance of rain
Three genuinely different answers for one day, which is the entire point. A dashboard that says "wear a jacket" no matter what you feed it is not worth opening.
A word on thresholds
There is no correct answer here. Some people are cold at 15 degrees and some are fine in a t-shirt. Every number in wardrobe.py is a guess, which is exactly why they are all constants at the top of the file rather than scattered through the logic.
Wear it wrong twice, adjust the number, and it becomes genuinely yours. That is a better outcome than a polished app that is confidently wrong about your particular metabolism.
Where to go from here
Change the city. place_id accepts names, or swap in lat and lon for anywhere in the world. Change TIMEZONE to match, or the slots will drift.
Add tomorrow. The Standard plan returns seven days of hourly data, so a second tab costs no extra API call.
Send it somewhere. The logic has no dependency on Streamlit. Wrap recommend() in a scheduled script and push the morning slot to your phone at 07:00, which is the point at which you actually make the decision.
Doing this on the free plan
The free plan's hourly forecast covers 24 hours and gives you temperature, wind.speed, cloud_cover.total and precipitation.total — but not feels_like, wind.gusts or probability.precipitation. The rules still work with a bit of rewiring: calculate wind chill yourself, key the wind advice off sustained speed with lower thresholds, and drop the umbrella-just-in-case line. You do get precipitation.type, which tells you when it is snow or freezing rain — worth a boots rule of its own. The Meteosource weather API is great for this project because it is very accurate.
Top comments (1)
Interesting approach! I've been working with free APIs too and discovered that handling errors gracefully is key. How do you handle API downtime?