<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Petrichor</title>
    <description>The latest articles on DEV Community by Petrichor (@petrichor_thunder).</description>
    <link>https://dev.to/petrichor_thunder</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4049316%2F5d03b570-4329-45e2-a182-18df2997eeb9.jpg</url>
      <title>DEV Community: Petrichor</title>
      <link>https://dev.to/petrichor_thunder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/petrichor_thunder"/>
    <language>en</language>
    <item>
      <title>Comparing weather across cities in Python: the timezone trap</title>
      <dc:creator>Petrichor</dc:creator>
      <pubDate>Wed, 26 Aug 2026 08:32:59 +0000</pubDate>
      <link>https://dev.to/petrichor_thunder/comparing-weather-across-cities-in-python-the-timezone-trap-5gnb</link>
      <guid>https://dev.to/petrichor_thunder/comparing-weather-across-cities-in-python-the-timezone-trap-5gnb</guid>
      <description>&lt;p&gt;Charting one city's forecast is a five-minute job. Charting four on the same axis raises a question single-city tutorials never ask: &lt;strong&gt;what does "the same time" mean across timezones?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a ~50-line Streamlit dashboard built on &lt;a href="https://www.meteosource.com/documentation?utm_source=devto&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Meteosource's weather API&lt;/a&gt;, using its &lt;code&gt;/point&lt;/code&gt; endpoint. &lt;a href="https://www.meteosource.com/pricing?utm_source=devto&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Free tier&lt;/a&gt;, 400 calls a day, no card.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;streamlit pandas requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The trap
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Two defensible answers, and you have to pick one on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Absolute time.&lt;/strong&gt; Request everything in UTC. "Right now, Barcelona is 17° warmer than Reykjavík." Correct for anything synchronised: flights, grid load, a live ops dashboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local time.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This one uses UTC, and you have to ask for it. Leave &lt;code&gt;timezone&lt;/code&gt; 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, &lt;a href="https://github.com/Meteosource/pymeteosource" rel="noopener noreferrer"&gt;pymeteosource&lt;/a&gt;, always requests UTC internally and converts afterwards: same problem, same conclusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamlit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;METEOSOURCE_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.meteosource.com/api/v1/free/point&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;CITIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prague&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;50.0755&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;14.4378&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Barcelona&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;41.3874&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.1686&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reykjavík&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;64.1466&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;21.9426&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Athens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;37.9838&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;23.7275&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🌍 City temperature comparison&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;chosen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;multiselect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CITIES&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prague&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Barcelona&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reykjavík&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="nd"&gt;@st.cache_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CITIES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# timezone=UTC is not the default, so it has to be explicit
&lt;/span&gt;    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sections&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hourly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timezone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;units&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metric&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hourly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;chosen&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Compare&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;frames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chosen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: no hourly data returned&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;wide&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;wide&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; °C&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;wide&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; °C low&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delta_color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;off&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;line_chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wide&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;caption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hourly temperature in °C. All timestamps UTC.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put your key in &lt;code&gt;.streamlit/secrets.toml&lt;/code&gt; as &lt;code&gt;METEOSOURCE_KEY = "..."&lt;/code&gt;, then &lt;code&gt;streamlit run app.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5y4xpqf6ez4wed90dz61.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5y4xpqf6ez4wed90dz61.png" alt="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" width="800" height="862"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Layout illustration with sample values. The free tier returns one day of hourly forecast, so your own chart will be shorter.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things worth noting
&lt;/h2&gt;

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

&lt;p&gt;&lt;code&gt;@st.cache_data(ttl=1800)&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Precipitation&lt;/strong&gt; lives at &lt;code&gt;precipitation.total&lt;/code&gt;, nested, so it needs &lt;code&gt;pd.json_normalize&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local-time alignment:&lt;/strong&gt; pass each city its own &lt;code&gt;timezone&lt;/code&gt; 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 &lt;code&gt;df[~df.index.duplicated(keep="first")]&lt;/code&gt; or &lt;code&gt;pd.concat&lt;/code&gt; will fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;place_id&lt;/code&gt; instead of coordinates:&lt;/strong&gt; resolve names via &lt;code&gt;/find_places_prefix&lt;/code&gt;. For mountains it gets you the peak's real elevation, where raw coordinates can land you lower down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't index by position.&lt;/strong&gt; The docs are explicit that sections and variables can grow over time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your cities seem to disagree about when the sun comes up, it's the timezone. It's always the timezone.&lt;/p&gt;

</description>
      <category>phyton</category>
      <category>tutorial</category>
      <category>api</category>
      <category>pandas</category>
    </item>
    <item>
      <title>What a weather API comparison table leaves out</title>
      <dc:creator>Petrichor</dc:creator>
      <pubDate>Mon, 17 Aug 2026 13:36:14 +0000</pubDate>
      <link>https://dev.to/petrichor_thunder/what-a-weather-api-comparison-table-leaves-out-3p98</link>
      <guid>https://dev.to/petrichor_thunder/what-a-weather-api-comparison-table-leaves-out-3p98</guid>
      <description>&lt;p&gt;A comparison table gets a provider onto your shortlist. Something else gets it picked.&lt;/p&gt;

&lt;p&gt;I answer the support inbox at &lt;a href="https://www.meteosource.com/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=weather_api_comparison" rel="noopener noreferrer"&gt;Meteosource&lt;/a&gt;, a weather API, so I read most messages from people in the middle of an evaluation. Price comes up early in almost all of them. It is the easiest thing to compare, so it gets compared first.&lt;/p&gt;

&lt;p&gt;It is rarely what settles it. Four other questions show up more often, and none of them are answered on a pricing page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I test this in the next five minutes
&lt;/h3&gt;

&lt;p&gt;Not features. Not accuracy. Whether a response can be on screen while the idea is still interesting.&lt;/p&gt;

&lt;p&gt;A card form on a free tier loses at this step. So does a sales call before an API key. So does a signup flow asking for company size and use case before it gives you anything.&lt;/p&gt;

&lt;p&gt;The message underneath all of it is the same one that kept landing in our inbox: I just want to see the JSON. That is why our free tier is 400 calls a day with no card, and it is the single change that moved trial signups the most.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does the license actually allow
&lt;/h3&gt;

&lt;p&gt;Plenty of free and cheap weather tiers are non commercial. Fine for a dashboard on your own wall. Not fine the moment your app has a paying user.&lt;/p&gt;

&lt;p&gt;Anyone caught by this once now reads the terms before the docs. It takes two minutes and it saves a migration later. Worth checking on any provider you shortlist, including us.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens when I hit the limit
&lt;/h3&gt;

&lt;p&gt;Three common answers, and they are not equally pleasant.&lt;/p&gt;

&lt;p&gt;A hard 429 tells you immediately. Silent throttling gives you degraded data and no signal, which is the worst version because your users find out before you do. Overage billing gives you a surprise invoice after a retry loop runs over a weekend.&lt;/p&gt;

&lt;p&gt;Predictable beats generous. A smaller cap you understand is easier to build on than a large one with fuzzy rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will this pricing still work in a year
&lt;/h3&gt;

&lt;p&gt;The pattern people are checking for is the cliff. A cheap starter plan, then nothing until an enterprise contract with a sales process attached.&lt;/p&gt;

&lt;p&gt;If usage doubles, does the bill roughly double, or does it jump by a factor of ten. Anyone who has migrated off a provider mid project looks for that gap before writing a line of code.&lt;/p&gt;

&lt;p&gt;We tried to avoid it by splitting features and call volume into separate choices, and by metering historical data per day per location instead of gating it behind a tier. It is not perfect. Our per location Renewables pricing genuinely does not suit anyone who wants to query a hundred scattered points occasionally, and a call based provider will be cheaper for that shape of usage. Different models fit different problems and pretending otherwise wastes everyone's time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why none of this shows up in a comparison table
&lt;/h3&gt;

&lt;p&gt;Because none of it is comparable. Signup friction, license scope, limit behaviour and pricing shape are all specific to how you plan to use the thing. There is no column for them.&lt;/p&gt;

&lt;p&gt;So they get left out, the table fills up with price and call counts, and the four questions that decide it end up as your own ten minutes of digging through docs and terms.&lt;/p&gt;

&lt;p&gt;That cuts both ways. As a provider you cannot write your way out of any of it. They are product and terms of service decisions, sitting in public, and the only way to look better on them is to be better on them.&lt;/p&gt;

&lt;p&gt;For anyone who has integrated a data API recently: what made you close the tab? Not the thing you complained about six months in, the thing that ruled a provider out on day one.&lt;/p&gt;

</description>
      <category>api</category>
      <category>software</category>
      <category>startup</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Finding the right price for a weather API</title>
      <dc:creator>Petrichor</dc:creator>
      <pubDate>Wed, 05 Aug 2026 12:23:04 +0000</pubDate>
      <link>https://dev.to/petrichor_thunder/finding-the-right-price-for-a-weather-api-2dei</link>
      <guid>https://dev.to/petrichor_thunder/finding-the-right-price-for-a-weather-api-2dei</guid>
      <description>&lt;h3&gt;
  
  
  &lt;em&gt;Why pricing a digital product is harder than it looks&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;Pricing a digital product is a strange kind of problem. There's no shelf to stock, and the cost of serving each customer isn't fixed. It grows with how much they use it. Usage is unpredictable, value is hard to see upfront, and a weather forecast can feel like a commodity, something buyers assume is all the same and shop for on price alone, even when the accuracy behind it isn't (in our case, 40 models blended with machine learning).&lt;/p&gt;

&lt;p&gt;Price it too high, and it's out of reach for a hobby developer building something small. Price it too low, and it can't support what a business running thousands of locations actually needs. We ran into this from three different angles at &lt;a href="https://www.meteosource.com/" rel="noopener noreferrer"&gt;Meteosource&lt;/a&gt;, and changed our pricing to deal with each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 1: usage is all over the place
&lt;/h3&gt;

&lt;p&gt;A subscription API was always our way of letting a solo developer and a global operations team build on the same forecasts. But a fixed call limit doesn't work for both. One project makes 5,000 calls a day, another makes 500,000. Same plan, wildly different needs.&lt;/p&gt;

&lt;p&gt;So to solve this, we split the offer into subscription levels, from Startup to Standard, and gave each level its own tier of call volumes. It works a bit like a table: pick your subscription for the features you need, then pick the call volume that matches how much you actually use, and find where you sit best. The details are on our &lt;a href="https://www.meteosource.com/pricing" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;, but the short version is: a side project and a scaling product don't have to fit in the same box anymore, and moving up as you grow is a lot more incremental.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 2: historical data doesn't fit neatly into a tier
&lt;/h3&gt;

&lt;p&gt;Twenty years of historical weather data is one of the most valuable things we offer, and one of the hardest to price fairly. Bundle it into a plan, and people download the archive once and leave. Put it behind its own tier, and the price climbs until only the biggest customers can justify it. Neither worked.&lt;/p&gt;

&lt;p&gt;We made it a metered add-on instead: $0.005 per day, per location, covering 20 years back, on any plan. You pay for exactly what you pull. A small project can dip into the archive without upgrading to an expensive tier, and the cost scales with actual usage instead of a flat guess.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 3: no tier existed for renewables forecasting
&lt;/h3&gt;

&lt;p&gt;We kept hearing from people forecasting energy production (solar and wind) who needed something none of our existing plans covered: power-production forecasts per location, at hourly or 15-minute resolution.&lt;/p&gt;

&lt;p&gt;So we built a Renewables tier from $19/location, covering PV systems and wind farms. It works whether you're modeling a few rooftop panels or a full farm. Past 20 sites, we set up custom pricing directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  What stayed the same
&lt;/h3&gt;

&lt;p&gt;Free is still 400 calls/day, no credit card, just an email. Enterprise is still fully custom, with parameters, data volume, SLAs, and applied-AI models built around the use case. Existing subscribers keep their current plan. We kept what was working, and moved around what needed a change.&lt;/p&gt;

&lt;p&gt;We'll keep adjusting further if the need appears, and of course keep adding features to cover more of what people actually need. That's really the goal behind all of it.&lt;/p&gt;

&lt;p&gt;But we're happy we cracked the logic of it, and it seems to be working well. What do you think about this change? What's your experience with the prices? Share it with us.&lt;/p&gt;

</description>
      <category>api</category>
      <category>saas</category>
      <category>weather</category>
      <category>weatherapi</category>
    </item>
  </channel>
</rss>
