Using OpenWeather Map API in a Python Flask App

Published on: January 2, 2025

OpenWeather provides weather services through APIs that you can use to display current, future, and historical weather conditions for a given place and time. In this tutorial, we will use their free service and build a simple Python Flask app that uses the OpenWeather One Call API 2.5 and the OpenWeather Geocoding API. Based on user input into a form, we will retrieve current weather data from OpenWeather and display some of that data in a browser.

Browser image

This tutorial assumes that you have Python installed, a code editor, used the Python command line interface to write commands, and a little experience with Python.

Get the API key

Before you start coding, you will need an API key, which is free and no credit card information is required. You need only to create an account, and an API key will be generated and emailed to you.

Go to the OpenWeather pricing page. Scroll down to the "Current weather and forecasts collection" section shown below. Under the "Free" heading, click Get API key.

OpenWeather pricing page

Complete the Create New Account form, and follow the instructions to verify your email. OpenWeather will email your API key to you. You can also sign in to the OpenWeather website and access your API key(s). Click your user name in the top right corner and choose My API keys (as shown below).

Accessing OpenWeather API key

Download project files

If you like, download the project files from GitHub and skip to Create your virtual environment.

Set up your directory structure

If you did not download the project files, create a new weather directory. Inside the weather directory, create the following structure:

Directory Structure

Download the weather icons

If you did not download the project files from GitHub and want to display the weather icons, get the images from OpenWeather and put them in the images folder.

Create your virtual environment

Creating a virtual environment for your project ensures that the packages you install will be specific to your weather project and will not create conflicts with other projects.

In your console, navigate to the weather directory. To create your virtual environment, in the console, input the command below for your operating system:

  • Windows:  python -m venv venv
  • macOS/Linux:  python3 -m venv venv

Python will create a venv directory inside the weather directory. This is where the packages will be installed.

Activate your virtual environment

To activate your virtual environment, input the command below for your operating system:

  • Windows:  .\venv\Scripts\activate
  • macOS/Linux:  source venv/bin/activate

In your console, you should see "(venv)" at the beginning of the command line, indicating your virtual environment is active.

Install the required libraries

You need only two libraries: Flask and Requests. Flask is used to build small- to medium-sized web applications in Python, and Requests is used to make HTTP requests. For detailed documentation, see Flask and Requests.

To install the packages, input the command below for your operating system:

  • Windows:  pip install Flask requests
  • macOS/Linux:  pip3 install Flask request

Write The Code

There are two files in this project:

  • templates/index.html: This file is stored in the templates directory. This is where the user will input a city, state two-letter abbreviation (for US only), and country two-letter abbreviation. When the user submits the form, the weather conditions are displayed below the form.
  • get_weather.py: This file will take the location information submitted in the index.html form and send it to the Geocoding API to get a corresponding longitude and latitude. Then it will use the longitude and latitude to request weather data from the One Call API. Finally, it will gather pieces of weather data and return that data to index.html for display.
  • index.html

    This is a very basic HTML file stored in your templates directory. Spruce it up with CSS or style sheets as you see fit. First add the document type declaration, head, and beginning body tag:

        <!DOCTYPE html>
        <html>
    
        <head>
            <title>Weather App</title>
        </head>
    
        <body>
        

    Next, we build the form the user will submit and the div element, weatherInfo, where the weather data will be displayed. Note the form action is not set. The JavaScript we include next will process the form and fetch the weather data from the get_weather.py script.

        <h2>Get Weather</h2>
    
        <form id="weatherForm" action="" method="get">
            <label for="city">City:</label>
            <input type="text" id="city" name="city">  
    
            <label for="state">State:</label>
            <input type="text" id="state" name="state" maxlength="2" size="2">  
    
            <label for="country">Country:</label>
            <input type="text" id="country" name="country" maxlength="2" size="2">  
    
            <button type="submit">Get Weather</button>
        </form>
    
        <div id="weatherInfo"></div>
        

    Now we write the JavaScript to listen for the form submit event, get the information (city, state, and country) input into the form, and create city, state, and country variables to hold that information.

        <script>
            const form = document.getElementById('weatherForm');
    
            form.addEventListener('submit', async (event) => {
                event.preventDefault();
    
                const city = document.getElementById('city').value;
                const state = document.getElementById('state').value;
                const country = document.getElementById('country').value;
    
        

    Next we use the city, state, and country variables to construct the URL, make a request to get_weather.py, and wait for the response. When we receive the response, we parse it as JSON and put it in the data variable.

                try {
                    const response = await fetch(`http://localhost:5000/get_weather?city=${city}&state=${state}&country=${country}`);
                    const data = await response.json();
        

    Once we have retrieved the data, we check for and display errors in the weatherInfo div. If there is no error, we format and display the location and weather details.

    
                    const weatherInfoDiv = document.getElementById('weatherInfo');
                    if (data.error) {
                        weatherInfoDiv.innerHTML = `<p>${data.error}</p>`;
                    } else {
                        weatherInfoDiv.innerHTML = `<h2>Weather in 
                        ${data.city}, ${data.state}, ${data.country}<</h2>
              <p>Temperature: ${data.temperature} °F</p> 
              <p>Winds: ${data.winds}</p>
              <p>Conditions: ${data.conditions}</p>>
              <p><img src="${data.icon_url}" alt="${data.conditions}"></p>`;
                    }
    
        

    Finally, we handle errors occurring during the network request or within the JavaScript code itself. If there is an error, we log it to the browser console and display a simple error message in the weatherInfo div for the user.

    
                } catch (error) {
                    console.error('Error fetching weather data:', error);
                    const weatherInfoDiv = document.getElementById('weatherInfo');
                    weatherInfoDiv.innerHTML = '<p>Error fetching weather data. 
                    Please try again.</p>';
                }
            });
            </script>
    
    </body>
    
    </html>
        

    get_weather.py

    This file is stored in the weather directory. To begin, we import the Flask and requests libraries.

    from flask import Flask, render_template, request, jsonify, url_for
    import requests
        

    Next, we create the Flask object app, including a definition of where template files and static files, such as images and CSS files, are stored. App routing defines what to return to the user upon coming to the app's root directory, in this case, index.html.

    #create flask object
    app = Flask(__name__, template_folder='templates', static_folder='static') 
    
    @app.route('/')
    def index():
        return render_template('index.html')
    
        

    Now, we define app routing for requests to the get_weather.py. We put the values from the request arguments (coming from index.html) into city, state, and country variables.

    @app.route('/get_weather')
    def get_weather():
        
        #process the arguments from the get request
        city = request.args.get('city')
        state = request.args.get('state')
        country = request.args.get('country')
    
        

    Next, we move on to the request to the Geocoding API. We build the URL, adding the city, state, country, and API key values. Replace YOUR_API_KEY with the API key you obtained from OpenWeather. Then:

    • Send the request and put the data received in the response variable.
    • If the status code received is not in the 200 range, raise an exception.
    • Parse the response as JSON and put it in the data variable.
        try:
            # Get latitude and longitude using Open Weather Map geocoding API
            api_key = "YOUR_API_KEY"
            url = f"http://api.openweathermap.org/geo/1.0/direct?q={city},{state},{country}&appid={api_key}"
            response = requests.get(url)
    
            response.raise_for_status()  # Raise an exception for bad status codes
    
            data = response.json()
        

    NOTE:
    If you query the Geocoding API directly in your browser, the JSON you get looks something like this.

    [
    
        {
        "name": "Portland",
        "local_names": {
            "bg": "Портланд",
            "da": "Portland",
            "ru": "Портленд",
            "mr": "पोर्टलंड",
            "sq": "Portland",
            "fa": "پورتلند",
            "nl": "Portland",
            "et": "Portland",
            "sl": "Portland",
            "tr": "Portland",
            "fo": "Portland",
            "hr": "Portland",
            "ta": "போர்ட்லன்ட்",
            "id": "Portland",
            "bn": "পোর্টল্যান্ড",
            "ja": "ポートランド",
            "ko": "포틀랜드",
            "el": "Πόρτλαντ",
            "pt": "Portland",
            "lv": "Portlenda",
            "te": "పోర్ట్ లాండ్",
            "ms": "Portland",
            "ro": "Portland",
            "en": "Portland",
            "ka": "Portland",
            "hu": "Portland",
            "lt": "Portlandas",
            "de": "Portland",
            "hy": "Պորտլենդ",
            "fy": "Portland",
            "tl": "Portland",
            "ca": "Portland",
            "es": "Portland",
            "fr": "Portland",
            "ku": "Portland",
            "be": "Портленд",
            "uk": "Портленд",
            "af": "Portland",
            "mk": "Портланд",
            "az": "Portlend",
            "ar": "بورتلاند",
            "sv": "Portland",
            "oc": "Portland",
            "sk": "Portland",
            "mg": "Portland",
            "th": "พอร์ตแลนด์",
            "fi": "Portland",
            "vi": "Portland",
            "hi": "पोर्टलैंड",
            "gu": "પોર્ટલેન્ડ"
        },
        "lat": 45.5202471,
        "lon": -122.674194,
        "country": "US",
        "state": "Oregon"
        }
    ]
    

    In the response data, what we are looking for are the lat and lon values. In the code, we take those values from the data variable and put them into the latitude and longitude variables. Multiple locations can be returned, so in the code below, we access the first location by the index [0].

    If no data is returned from the Geocoding API, we return an error to index.html.

            if len(data) > 0:
                latitude = data[0]['lat']
                longitude = data[0]['lon']
            else:
                return jsonify({"error": "Location not found."}), 404
            

    With the latitude and longitude set, we construct the URL for the One Call API. In the code below, I have added the argument &units=imperial to the end of the URL. You can also set units to "metric" or "standard." If you exclude the units argument, the default unit is standard.

           base_url = f"https://api.openweathermap.org/data/2.5/weather?lat={latitude}&lon={longitude}&appid={api_key}&units=imperial" 
            

    NOTE:
    If you query the One Call API in your browser, you will get JSON back, as we did with the Geocoding API. The JSON contains many weather details you can access. A formatted sample response is below.

    {
    
        "coord": {
            "lon": -122.6742,
            "lat": 45.5202
        },
        "weather": [
            {
                "id": 803,
                "main": "Clouds",
                "description": "broken clouds",
                "icon": "04d"
            }
        ],
        "base": "stations",
        "main": {
            "temp": 44.89,
            "feels_like": 41.04,
            "temp_min": 42.53,
            "temp_max": 46.63,
            "pressure": 1029,
            "humidity": 95,
            "sea_level": 1029,
            "grnd_level": 1018
        },
        "visibility": 10000,
        "wind": {
            "speed": 7,
            "deg": 213,
            "gust": 11.01
        },
        "clouds": {
            "all": 75
        },
        "dt": 1735586676,
        "sys": {
            "type": 2,
            "id": 2013569,
            "country": "US",
            "sunrise": 1735573844,
            "sunset": 1735605360
        },
        "timezone": -28800,
        "id": 5746545,
        "name": "Portland",
        "cod": 200
    }
            

    We send the request using the URL constructed above and put the data received in the response variable. We raise an exception if the status code received is not in the 200 range. We parse the response as JSON and put it in the weather_data variable.

            response = requests.get(base_url)
            response.raise_for_status()  # Raise an exception for bad status codes
            weather_data = response.json()
            

    We put the wind speed, wind direction (in degrees), and the weather icon from weather_data into variables. These pieces of information require some formatting:

    • Convert the wind direction, wind_deg, into a friendly text version (e.g. NE or SW) using the get_wind_direction function created further down.
    • Put the weather_icon in the url_for function to construct a URL for the icon. The icons are located in the static/images directory. If you did not download the project files, you can get them directly from OpenWeather here.
            wind_speed = weather_data["wind"]["speed"]
            wind_deg = weather_data["wind"]["deg"]
            weather_icon = weather_data["weather"][0]["icon"]
            wind_direction = get_wind_direction(wind_deg) 
            wind_speed_with_direction = f"{wind_speed} m/s {wind_direction}"
            weather_icon_url = url_for('static', filename=f'images/{weather_icon}@2x.png')
            

    Now, we use the wind_speed_with_direction, weather_icon_url, city, state, and country variables to build a Python dictionary. We also include the temperature and weather description data from the weather_data JSON. Lastly, we return the weather_data to index.html as JSON for display.

            weather_data = {
                "temperature": weather_data["main"]["temp"], 
                "conditions": weather_data["weather"][0]["description"],
                "icon_url": weather_icon_url,
                "winds": wind_speed_with_direction,
                "city": city,
                "state": state,
                "country": country
            }
    
            return jsonify(weather_data)
            

    To wrap it up, we:

    • Handle any exceptions that occurred in the try block of code.
    • Create the get_wind_direction function.
    • Set the Flask development server to only start if the script is run directly. Remove these last two lines of code in a production environment.
        except requests.exceptions.RequestException as e:
            return jsonify({"error": f"Error fetching weather data: {e}"}), 500
    
    def get_wind_direction(degrees):
        directions = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", 
                      "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
        index = int((degrees + 11.25) / 22.5) % 16 
        return directions[index]
    
    if __name__ == "__main__":
        app.run(debug=True) 
            

    Run the Flask app in your browser

    To run the Flask app in your browser, start the Flask server. In your console, type the following command:

    • flask --app get_weather run

    Open your browser and in the address bar, type: localhost:5000. You should see the form to input a city, state, and country. Submit the form, and the weather data should display below the form.

    Browser image

    More on OpenWeather APIs

    OpenWeather is moving toward requiring credit card information for all subscriptions. The One Call API 2.5 remains available but is being deprecated. The documentation is here.

    The documentation for current API versions is available here.

    What next?

    Now that you have created a simple Flask app, I recommend reading more on Flask, JSON, and Requests.