Fetch, render and send as three modules
Email a weather forecast on a schedule, split into three modules that each do one thing.
weather.py fetches, mailer.py sends, emailer.py orchestrates, with
recipients in emails.txt and timing in schedule.txt.
For a program this small the split looks like overhead, and it is the clearest small example of why it pays. The forecast source can be swapped without touching delivery. More usefully, delivery can be exercised without calling the weather API at all, which means testing the part that actually breaks does not depend on an external service being up or on burning quota.
The flat-file configuration is a genuine trade rather than laziness.
emails.txt and schedule.txt are editable by someone who does not read
Python, which for a tool other people might inherit is worth something. What
they lack is validation: a malformed line is discovered at run time, in whatever
way the parser happens to fail.
The failure I would handle differently is per-recipient. A batch send where one address is bad should skip it and continue and record which failed. The naive version aborts partway with no record of who received it, so the retry either double-sends or skips people.
schedule.txt alongside cron is two sources of truth for when the job runs.