Home » Tutorials » How to Build a Weather App with Flask in Python

How to Build a Weather App with Flask in Python

Have you ever wondered how you could make your very own weather application? It’s actually quite an exciting project that combines the magic of coding with the practicality of real-time weather updates. Using Flask, a lightweight Python web framework.

Today, we’ll dive into creating a web app using Flask that brings us the latest weather. You’ll learn the fundamentals of setting up a Flask project, obtaining an API key from Open Weather Map, and integrating weather data into your application.

Let’s get started!

Table of Contents

Necessary Libraries

Let’s get everything set up before we jump into the code, so make sure to install the Flask and requests libraries via the terminal or your command prompt for the code to function properly:

$ pip install flask 
$ pip install requests

Before we Start

We need to create a ‘templates‘ file directory to contain our HTML files and a ‘static‘ file directory to contain our cascading style sheets (CSS). 

Open Weather Map API Key

An Open Weather Map API key is a unique code that grants your app access to real-time and forecasted weather data from their database. You get it by signing up on their website, allowing you to pull weather information into your application.

Check out this tutorial where we explain how to sign up and get an API key from OpenWeatherMap.

Once you have done all this just copy your API key and paste it into your code.

This image has an empty alt attribute; its file name is image1-min-8-1024x376.png

Imports

We start by importing the Flask main class to customize it later, and render_template function which allows us to render HTML templates, and also the request module to handle incoming HTTP requests (GET, POST in the case of this script) within the flask application (mainly for HTML, and browser).

from flask import Flask, render_template, request

Then we import the requests library which is used to send HTTP requests from the Python application to external servers (in this case the OpenWeatherMap API) to retrieve data.

import requests

Finally, we import datetime so that we can work with date and time.

import datetime

Initializing Flask Application

Next, we create a Flask application instance unlocking the door to customize the Flask main class.

app = Flask(__name__)

Setting OpenWeatherMap API Key

For this step, we create a variable that will store the API key that we need to access the OpenWeatherMap data.

# OpenWeatherMap API key
API_KEY = 'Enter_Your_API_KEY'

Defining Routes

Now, let’s define our routes:

Index Route

The “index route” defines the URL for the homepage. When the user visits it, the index() function is called, rendering the “index.html” template to display the homepage.

Learn more about routes in this tutorial.

@app.route('/')
def index():
   return render_template('index.html')

Weather Route

This one defines The URL that takes us to the result, basically, when the POST request is made, meaning when the user submits the city name the weather() function is called, and it retrieves the city’s name. After that, it builds a URL for the OpenWeatherMap API with the city name to acquire the weather data through the GET request, then it converts the JSON response from the API into a data structure.

@app.route('/weather', methods=['POST'])
def weather():
   city = request.form['city']
   # Current weather API call
   current_url = f'http://api.openweathermap.org/data/2.5/weather?q={city}&appid={API_KEY}&units=metric'
   current_response = requests.get(current_url)
   current_data = current_response.json()

Handling Current Weather Data

With the stage ready, this part verifies whether the API response is successful by checking if the status code for the current weather data is 200. which means everything’s good. If it’s successful, it extracts relevant information such as city, country, temperature, description, weather icon, wind speed, pressure, humidity, sunrise time, and sunset time from the JSON response and formats them into a dictionary called weather_data.

The sunrise and sunset times are converted by the datetime module from the JSON response to be readable.

if current_data['cod'] == 200:
   weather_data = {
       'city': city,
       'country': current_data['sys']['country'],
       'temperature': current_data['main']['temp'],
       'description': current_data['weather'][0]['description'],
       'icon': current_data['weather'][0]['icon'],
       'wind_speed': current_data['wind']['speed'],
       'pressure': current_data['main']['pressure'],
       'humidity': current_data['main']['humidity'],
       'sunrise': datetime.datetime.fromtimestamp(current_data['sys']['sunrise']).strftime('%H:%M:%S'),
       'sunset': datetime.datetime.fromtimestamp(current_data['sys']['sunset']).strftime('%H:%M:%S'),
       'day': datetime.datetime.now().strftime('%A'),
       'date': datetime.datetime.now().strftime('%Y-%m-%d')
   }

Handling Errors

Now to handle errors, this section of the code is triggered if the API response is not successful (not 200), the code retrieves the error message from the response and then renders the “error.html” template, passing the error message as a parameter to be displayed to the user.

else:
   error_message = current_data['message']
   return render_template('error.html', error_message=error_message)

Handling Weekly Forecast Data

This one functions exactly the same as the weather route, with the only difference being the type of weather data it seeks to acquire. While the previous one requests current relevant data, this one aims for the weekly relevant data.

# Weekly forecast API call
forecast_url = f'http://api.openweathermap.org/data/2.5/forecast?q={city}&appid={API_KEY}&units=metric'
forecast_response = requests.get(forecast_url)
forecast_data = forecast_response.json()

Passing Weekly Forecast Data

Here, we start by setting up a list called weekly_forecast to hold the forecast data for the week. The process kicks off by verifying the success of the response from the OpenWeatherMap API. Upon successful retrieval, each forecast item is processed to extract key details like temperature, humidity, wind speed, weather description, and icon.

These details are organized by day and date, ensuring we gather comprehensive information for each day of the week. Once processed, these daily summaries are added to our weekly_forecast list, building a complete picture of the week’s weather.

weekly_forecast = []
if forecast_data['cod'] == '200':
   current_date = None
   forecast_group = None
   for forecast in forecast_data['list']:
       forecast_date = datetime.datetime.fromtimestamp(forecast['dt']).strftime('%Y-%m-%d')
       forecast_day = datetime.datetime.fromtimestamp(forecast['dt']).strftime('%A')
       if forecast_date != current_date:
           if forecast_group:
               weekly_forecast.append(forecast_group)
           forecast_group = {
               'day': forecast_day,
               'date': forecast_date,
               'temperature': forecast['main']['temp'],
               'humidity': forecast['main']['humidity'],
               'wind_speed': forecast['wind']['speed'],
               'description': forecast['weather'][0]['description'],
               'icon': forecast['weather'][0]['icon'],
               'forecasts': []
           }
           current_date = forecast_date
       forecast_item = {
           'temperature': forecast['main']['temp'],
           'humidity': forecast['main']['humidity'],
           'wind_speed': forecast['wind']['speed'],
           'description': forecast['weather'][0]['description'],
           'icon': forecast['weather'][0]['icon']
       }
       forecast_group['forecasts'].append(forecast_item)
   if forecast_group:
       weekly_forecast.append(forecast_group)

Error Handling for Weekly Forecast Data

This part is triggered if the response for the OpenWeatherMap API for the weekly forecast is not successful, it works the exact same way for handling errors for current weather data.

else:
   error_message = forecast_data['message']
   return render_template('error.html', error_message=error_message)

Rendering the Weather Template

Once the current weather data and the weekly forecast data are successfully retrieved, this part is triggered, it renders the “weather.html” template and displays the current weather data and the weekly forecast data on the webpage.

return render_template('weather.html', weather=weather_data, weekly_forecast=weekly_forecast)

Flask Application Execution

Lastly, this block ensures that the script is run directly and not imported as a module, as well as enabling debugging which allows us to see detailed error messages and to automatically reload the server when we make changes to the code during the development.

if __name__ == '__main__':
   app.run(debug=True)

Full Code

from flask import Flask, render_template, request
import requests, datetime


app = Flask(__name__)


# OpenWeatherMap API key
API_KEY = 'Enter_Your_API_KEY'


@app.route('/')
def index():
   return render_template('index.html')


@app.route('/weather', methods=['POST'])
def weather():
   city = request.form['city']
   # Current weather API call
   current_url = f'http://api.openweathermap.org/data/2.5/weather?q={city}&appid={API_KEY}&units=metric'
   current_response = requests.get(current_url)
   current_data = current_response.json()


   if current_data['cod'] == 200:
       weather_data = {
           'city': city,
           'country': current_data['sys']['country'],
           'temperature': current_data['main']['temp'],
           'description': current_data['weather'][0]['description'],
           'icon': current_data['weather'][0]['icon'],
           'wind_speed': current_data['wind']['speed'],
           'pressure': current_data['main']['pressure'],
           'humidity': current_data['main']['humidity'],
           'sunrise': datetime.datetime.fromtimestamp(current_data['sys']['sunrise']).strftime('%H:%M:%S'),
           'sunset': datetime.datetime.fromtimestamp(current_data['sys']['sunset']).strftime('%H:%M:%S'),
           'day': datetime.datetime.now().strftime('%A'),
           'date': datetime.datetime.now().strftime('%Y-%m-%d')
       }
   else:
       error_message = current_data['message']
       return render_template('error.html', error_message=error_message)


   # Weekly forecast API call
   forecast_url = f'http://api.openweathermap.org/data/2.5/forecast?q={city}&appid={API_KEY}&units=metric'
   forecast_response = requests.get(forecast_url)
   forecast_data = forecast_response.json()


   weekly_forecast = []
   if forecast_data['cod'] == '200':
       current_date = None
       forecast_group = None
       for forecast in forecast_data['list']:
           forecast_date = datetime.datetime.fromtimestamp(forecast['dt']).strftime('%Y-%m-%d')
           forecast_day = datetime.datetime.fromtimestamp(forecast['dt']).strftime('%A')
           if forecast_date != current_date:
               if forecast_group:
                   weekly_forecast.append(forecast_group)
               forecast_group = {
                   'day': forecast_day,
                   'date': forecast_date,
                   'temperature': forecast['main']['temp'],
                   'humidity': forecast['main']['humidity'],
                   'wind_speed': forecast['wind']['speed'],
                   'description': forecast['weather'][0]['description'],
                   'icon': forecast['weather'][0]['icon'],
                   'forecasts': []
               }
               current_date = forecast_date
           forecast_item = {
               'temperature': forecast['main']['temp'],
               'humidity': forecast['main']['humidity'],
               'wind_speed': forecast['wind']['speed'],
               'description': forecast['weather'][0]['description'],
               'icon': forecast['weather'][0]['icon']
           }
           forecast_group['forecasts'].append(forecast_item)
       if forecast_group:
           weekly_forecast.append(forecast_group)
   else:
       error_message = forecast_data['message']
       return render_template('error.html', error_message=error_message)


   return render_template('weather.html', weather=weather_data, weekly_forecast=weekly_forecast)


if __name__ == '__main__':
   app.run(debug=True)

Styles CSS

/* styles.css */
body {
   font-family: Arial, sans-serif;
   background-color: #f0f0f0;
   margin: 0;
   padding: 0;
}

.container {
   max-width: 600px;
   margin: 50px auto;
   padding: 20px;
   background-color: #fff;
   border-radius: 10px;
   box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

h1 {
   text-align: center;
   margin-bottom: 20px;
}

form {
   text-align: center;
}

label {
   font-weight: bold;
}

input[type="text"] {
   width: 100%;
   padding: 10px;
   margin-top: 5px;
   margin-bottom: 20px;
   border: 1px solid #ccc;
   border-radius: 5px;
}

button {
   padding: 10px 20px;
   background-color: #007bff;
   color: #fff;
   border: none;
   border-radius: 5px;
   cursor: pointer;
   transition: background-color 0.3s;
}

button:hover {
   background-color: #0056b3;
}

.weather-info {
   margin-bottom: 20px;
}

img {
   display: block;
   margin: 0 auto;
}

Here we are going to create a cascading Style Sheets file (CSS) which is basically a designer for our webpage.

Body Styling:

As you can infer from the code in this part we set the font to Arial, the background color to light gray, and Reset the margin and the padding to 0.

body {
   font-family: Arial, sans-serif;
   background-color: #f0f0f0;
   margin: 0;
   padding: 0;
}

Container Styling:

Here we defined the elements of the container such as the maximum width, the margin, the padding, the background color, and the box-shadow.

.container {
   max-width: 600px;
   margin: 50px auto;
   padding: 20px;
   background-color: #fff;
   border-radius: 10px;
   box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

Heading Styling:

Here we aligned all the headings to be at the center and added some margin between headings and other content.

h1 {
   text-align: center;
   margin-bottom: 20px;
}

Form Styling and Label:

We aligned all the content in the center and the label font bold.

form {
   text-align: center;
}

label {
   font-weight: bold;
}

Input Field Styling:

As the title suggests we’re going to style the input fields of the texts, so basically we are going to define the width, padding, margin-top, margin-bottom, border and its color, and border-radius.

input[type="text"] {
   width: 100%;
   padding: 10px;
   margin-top: 5px;
   margin-bottom: 20px;
   border: 1px solid #ccc;
   border-radius: 5px;
}

Button Styling:

Here we are going to set padding inside the button for spacing and give it the color blue then we will set the text inside the button as white and remove the button border while still applying a border-radius, and then we will change the cursor to a pointer on hover.

button {
   padding: 10px 20px;
   background-color: #007bff;
   color: #fff;
   border: none;
   border-radius: 5px;
   cursor: pointer;
   transition: background-color 0.3s;
}

button:hover {
   background-color: #0056b3;
}

Image Styling:

This part gives a margin-bottom to the weather info as well as displays the images as blocks.

.weather-info {
   margin-bottom: 20px;
}

img {
   display: block;
   margin: 0 auto;
}

HTML Templates

Index HTML

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Weather App - The Pycodes</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
   <div class="container">
       <h1>The Pycodes</h1>
       <h1>Weather App</h1>
       <form action="/weather" method="post">
           <label for="city">Enter city name:</label>
           <input type="text" id="city" name="city" required>
           <button type="submit">Get Weather</button>
       </form>
   </div>
</body>
</html>

Document Structure:

This part pretty much declares the document as HTML and sets the document language to English, then we will go straight to the head which specifies the character encoding of the document as well as ensures this template is rendered on various devices.

Next, we set the title for the homepage. Finally, we link the “index.html” to the “styles.css” file for better visual appeal.

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Weather App - The Pycodes</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>

Body Content:

This part concerns the content of the homepage that will be contained in the container we styled in the “styles.css” file, so in this part we defined two headings one for the homepage and the other for the app, then using the post method we allow the user to input the city name and submit it with the button we styled earlier, once that is done we will get the weather information.

<body>
   <div class="container">
       <h1>The Pycodes</h1>
       <h1>Weather App</h1>
       <form action="/weather" method="post">
           <label for="city">Enter city name:</label>
           <input type="text" id="city" name="city" required>
           <button type="submit">Get Weather</button>
       </form>
   </div>
</body>

Closing Tags:

This part indicates the end of the HTML.

</html>

Weather HTML

<!-- weather.html -->
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Weather - The Pycodes</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
   <div class="container">
       <h1>Current Weather in {{ weather.city }}, {{ weather.country }}</h1>
       <p>{{ weather.day }}, {{ weather.date }}</p>
       <div class="weather-info">
           <p>Temperature: {{ weather.temperature }}°C</p>
           <p>Description: {{ weather.description }}</p>
           <p>Wind Speed: {{ weather.wind_speed }} m/s</p>
           <p>Pressure: {{ weather.pressure }} hPa</p>
           <p>Humidity: {{ weather.humidity }}%</p>
           <p>Sunrise: {{ weather.sunrise }}</p>
           <p>Sunset: {{ weather.sunset }}</p>
       </div>
       <img src="http://openweathermap.org/img/wn/{{ weather.icon }}.png" alt="Weather Icon">
   </div>
   <div class="container">
       <h2>Weekly Forecast</h2>
       <div class="forecast">
           {% for forecast_group in weekly_forecast %}
               <div class="forecast-group">
                   <h3>{{ forecast_group.day }}, {{ forecast_group.date }}</h3>
                   <div class="forecast-item">
                       <p>Temperature: {{ forecast_group.temperature }}°C</p>
                       <p>Humidity: {{ forecast_group.humidity }}%</p>
                       <p>Wind Speed: {{ forecast_group.wind_speed }} m/s</p>
                       <p>Description: {{ forecast_group.description }}</p>
                   </div>
                   {% for forecast in forecast_group.forecasts %}
                       <div class="forecast-item">
                           <img src="http://openweathermap.org/img/wn/{{ forecast.icon }}.png" alt="Weather Icon">
                       </div>
                   {% endfor %}
               </div>
           {% endfor %}
       </div>
   </div>
</body>
</html>

Document Structure:

The exact same explanation as the “index.html” the only difference is the title of the webpage.

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Weather - The Pycodes</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>

Body Content:

This part contains the content for the weather page which basically is the extracted information, in other words, this HTML will display the current weather in the inputted city by the user along with an appropriate heading, as well as display the weekly_forecast list data with the appropriate heading, and also display weather icons for the description. Needless to say, all this content is in the container styled in the “styles.css” file.

<body>
   <div class="container">
       <h1>Current Weather in {{ weather.city }}, {{ weather.country }}</h1>
       <p>{{ weather.day }}, {{ weather.date }}</p>
       <div class="weather-info">
           <p>Temperature: {{ weather.temperature }}°C</p>
           <p>Description: {{ weather.description }}</p>
           <p>Wind Speed: {{ weather.wind_speed }} m/s</p>
           <p>Pressure: {{ weather.pressure }} hPa</p>
           <p>Humidity: {{ weather.humidity }}%</p>
           <p>Sunrise: {{ weather.sunrise }}</p>
           <p>Sunset: {{ weather.sunset }}</p>
       </div>
       <img src="http://openweathermap.org/img/wn/{{ weather.icon }}.png" alt="Weather Icon">
   </div>
   <div class="container">
       <h2>Weekly Forecast</h2>
       <div class="forecast">
           {% for forecast_group in weekly_forecast %}
               <div class="forecast-group">
                   <h3>{{ forecast_group.day }}, {{ forecast_group.date }}</h3>
                   <div class="forecast-item">
                       <p>Temperature: {{ forecast_group.temperature }}°C</p>
                       <p>Humidity: {{ forecast_group.humidity }}%</p>
                       <p>Wind Speed: {{ forecast_group.wind_speed }} m/s</p>
                       <p>Description: {{ forecast_group.description }}</p>
                   </div>
                   {% for forecast in forecast_group.forecasts %}
                       <div class="forecast-item">
                           <img src="http://openweathermap.org/img/wn/{{ forecast.icon }}.png" alt="Weather Icon">
                       </div>
                   {% endfor %}
               </div>
           {% endfor %}
       </div>
   </div>

Closing Tags:

This part indicates the end of the HTML.

</body>
</html>

Error HTML

<!-- error.html -->
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Error</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
   <div class="container">
       <h1>Error</h1>
       <p>{{ error_message }}</p>
       <a href="/">Go back to homepage</a>
   </div>
</body>
</html>

Document Structure:

The exact same as the two previous HTMLs the only difference being the title.

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Error</title>
   <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>

Body Content:

This section includes an error heading, an error message, and a link to the homepage, all contained within a style defined in the “styles.css” file.

<body>
   <div class="container">
       <h1>Error</h1>
       <p>{{ error_message }}</p>
       <a href="/">Go back to homepage</a>
   </div>
</body>

Closing Tags:

This part indicates the end of the HTML.

</html>

Example

Let’s try to put wrong city:

Wrapping Up

Making your own weather app with Flask is a cool mix of coding fun and getting real-time weather updates. We looked at how to start with Flask, get an API key from Open Weather Map, and then use that data in your app. It’s an awesome journey into coding by building something useful and exciting!

Happy Coding!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top