Python • APIs • Weather.gov • Terminal Tools

Tiny Terminal Weather App

This project turns the weather logic from my larger Flask seed app into a small standalone terminal tool. The app takes a ZIP code or latitude/longitude, asks Weather.gov for the forecast, parses the JSON response, and prints the next few forecast periods in the terminal.

I like this project because it makes APIs feel real. Instead of opening a weather site, my own Python script talks to Weather.gov, follows the API-provided forecast URL, and prints a clean result I can use from the command line.

Project context: this is a tiny version of a real feature from my seed tracking app. The larger app uses weather data for garden planning, but this project keeps the core API idea small enough to understand in one file.
Terminal command Run the script with a ZIP code or coordinates.
Weather.gov /points Convert coordinates into the correct forecast endpoint.
JSON forecast Read forecast periods from the API response.
Clean terminal output Print the next three forecast periods.
A small terminal script can turn a public API into a useful everyday tool.

What I built

I built a one-file Python weather app called weather.py. It uses only the Python standard library, so there are no pip packages, no framework, and no API key required.

Python

Standard library only

The script uses urllib, json, argparse, os, and basic error handling.

API

Weather.gov forecast data

The app calls the Weather.gov API, follows the returned forecast URL, and reads the forecast periods from the JSON response.

CLI

Terminal output

The result is printed directly in the terminal, which makes it simple to use over SSH, in a VM, on a Raspberry Pi, or inside a Codespace.

Why this project matters

This is a small project, but it teaches the core API workflow: send a request, get JSON back, extract useful fields, and turn the response into something readable.

01

Call an API

Use Python to request data from a real public web service.

02

Parse JSON

Read nested dictionaries and lists from the API response.

03

Handle errors

Catch HTTP errors, network errors, bad input, and missing data.

04

Build a tool

Turn raw API data into a useful command-line app.

How this connects to my seed app

My larger seed tracking app already uses a similar weather pipeline. This tiny project pulls out the core idea and makes it easier to study.

Seed app idea Tiny app version Why it matters
Weather User-Agent WEATHER_USER_AGENT environment variable. Identifies the app when making Weather.gov requests.
fetch_json() Reusable helper for API requests. Keeps headers, timeouts, and JSON parsing in one place.
ZIP lookup Convert a ZIP code into latitude and longitude. Lets the user run the app with a simple ZIP code.
Weather.gov /points Follow the forecast URL returned by Weather.gov. Avoids hardcoding the forecast office and gridpoint manually.
Forecast periods Print the next three periods. Keeps the output small and readable.

How it works

The app follows the same basic API workflow every time.

01

Read input

The user provides either a ZIP code or latitude and longitude.

02

Resolve location

If a ZIP code is used, the app converts it to coordinates.

03

Ask Weather.gov

The app calls the Weather.gov /points endpoint.

04

Print forecast

The app reads forecast periods and prints the next few results.

Main lesson: APIs often return links to other API endpoints. The first request does not always contain the final data. Sometimes it tells the program where to go next.

Project files

This project can be kept very small.

tiny-terminal-weather/
├── weather.py
└── README.md

Run it

The script can be run directly with Python.

python3 weather.py --zip 11201

It can also use coordinates directly.

python3 weather.py --coords 40.7128 -74.0060

By default, it prints three forecast periods. The count can be changed.

python3 weather.py --zip 11201 --count 5

Example output

The exact weather changes, but the output format stays simple.

Weather forecast for New York, NY
=================================

Today
-----
Temp: 72°F
Wind: 8 mph NW
Short: Mostly Sunny
Details: Mostly sunny, with a high near 72.

Tonight
-------
Temp: 58°F
Wind: 6 mph N
Short: Partly Cloudy
Details: Partly cloudy, with a low around 58.

Tuesday
-------
Temp: 74°F
Wind: 7 mph E
Short: Chance Rain Showers
Details: A chance of rain showers after 2pm.

The full Python script

This is the complete turnkey version. Save it as weather.py.

#!/usr/bin/env python3

import argparse
import json
import os
import re
import sys
import urllib.error
import urllib.request


DEFAULT_USER_AGENT = os.environ.get(
    "WEATHER_USER_AGENT",
    "tiny-weather/1.0 (student project; contact: example@example.com)"
)


def fetch_json(url):
    request_headers = {
        "Accept": "application/geo+json, application/json",
        "User-Agent": DEFAULT_USER_AGENT,
    }

    req = urllib.request.Request(url, headers=request_headers)

    with urllib.request.urlopen(req, timeout=15) as response:
        return json.loads(response.read().decode("utf-8"))


def fetch_zip_location(zip_code):
    zip_code = re.sub(r"[^0-9]", "", zip_code or "")

    if len(zip_code) != 5:
        raise ValueError("Use a valid 5 digit US ZIP code.")

    data = fetch_json(f"https://api.zippopotam.us/us/{zip_code}")
    place = data.get("places", [{}])[0]

    return {
        "zip_code": zip_code,
        "latitude": place["latitude"],
        "longitude": place["longitude"],
        "label": f"{place.get('place name', zip_code)}, {place.get('state abbreviation', '')}".strip(", "),
    }


def fetch_forecast_url(latitude, longitude):
    points_url = f"https://api.weather.gov/points/{latitude},{longitude}"
    data = fetch_json(points_url)

    properties = data.get("properties", {})
    forecast_url = properties.get("forecast")

    if not forecast_url:
        raise ValueError("Weather.gov did not return a forecast URL for this location.")

    location = properties.get("relativeLocation", {}).get("properties", {})
    city = location.get("city", "")
    state = location.get("state", "")

    return forecast_url, f"{city}, {state}".strip(", ")


def fetch_forecast_periods(latitude, longitude):
    forecast_url, location_label = fetch_forecast_url(latitude, longitude)
    forecast = fetch_json(forecast_url)

    periods = forecast.get("properties", {}).get("periods", [])

    if not periods:
        raise ValueError("Weather.gov did not return forecast periods.")

    return location_label, periods


def print_forecast(location_label, periods, count):
    print()
    print(f"Weather forecast for {location_label}")
    print("=" * (21 + len(location_label)))

    for period in periods[:count]:
        name = period.get("name", "Forecast")
        temperature = period.get("temperature", "")
        unit = period.get("temperatureUnit", "")
        wind_speed = period.get("windSpeed", "")
        wind_direction = period.get("windDirection", "")
        short_forecast = period.get("shortForecast", "")
        detailed_forecast = period.get("detailedForecast", "")

        print()
        print(name)
        print("-" * len(name))
        print(f"Temp: {temperature}°{unit}")
        print(f"Wind: {wind_speed} {wind_direction}".strip())
        print(f"Short: {short_forecast}")

        if detailed_forecast:
            print(f"Details: {detailed_forecast}")

    print()


def main():
    parser = argparse.ArgumentParser(
        description="Tiny terminal weather app using Weather.gov."
    )

    location = parser.add_mutually_exclusive_group(required=True)

    location.add_argument(
        "--zip",
        dest="zip_code",
        help="US ZIP code, example: 11201"
    )

    location.add_argument(
        "--coords",
        nargs=2,
        metavar=("LATITUDE", "LONGITUDE"),
        help="Latitude and longitude, example: 40.7128 -74.0060"
    )

    parser.add_argument(
        "-n",
        "--count",
        type=int,
        default=3,
        help="Number of forecast periods to print. Default: 3"
    )

    args = parser.parse_args()

    try:
        if args.zip_code:
            location_info = fetch_zip_location(args.zip_code)
            latitude = location_info["latitude"]
            longitude = location_info["longitude"]
            fallback_label = location_info["label"]
        else:
            latitude, longitude = args.coords
            fallback_label = f"{latitude}, {longitude}"

        location_label, periods = fetch_forecast_periods(latitude, longitude)

        if not location_label:
            location_label = fallback_label

        print_forecast(location_label, periods, max(args.count, 1))

    except urllib.error.HTTPError as error:
        print(f"HTTP error: {error.code} {error.reason}", file=sys.stderr)
        sys.exit(1)

    except urllib.error.URLError as error:
        print(f"Network error: {error.reason}", file=sys.stderr)
        sys.exit(1)

    except (KeyError, ValueError, TimeoutError) as error:
        print(f"Weather lookup failed: {error}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()

Make it executable

Because the file starts with a shebang, it can also run like a small Linux command.

chmod +x weather.py
./weather.py --zip 11201
Linux lesson: this turns a Python file into a command-line tool. That is the same basic pattern used for many small admin scripts.

Customize the User-Agent

Weather.gov expects a User-Agent that identifies the application. The script uses a default, but I can override it with an environment variable.

export WEATHER_USER_AGENT="tiny-weather/1.0 (your-email@example.com)"
python3 weather.py --zip 11201
Public project note: do not put private emails, home addresses, API keys, or secrets into screenshots or public examples.

What the important functions do

The app is easier to understand when each function has one job.

Function Job Why it exists
fetch_json() Sends the HTTP request and parses JSON. Keeps request headers, timeout, and JSON loading in one helper.
fetch_zip_location() Turns a ZIP code into coordinates. Lets the user run the app with a simple ZIP code.
fetch_forecast_url() Calls Weather.gov /points. Finds the correct forecast endpoint for the location.
fetch_forecast_periods() Downloads the actual forecast periods. Separates location lookup from forecast retrieval.
print_forecast() Formats the output for the terminal. Turns raw JSON fields into readable text.
main() Handles arguments and errors. Keeps the script usable as a normal command-line tool.

Why this is safer than a frontend API demo

This specific app does not need a secret API key, but the project still teaches an important security habit: API logic often belongs in scripts or backend code, not exposed frontend code.

Good pattern

Terminal or backend code

The request is made from Python, where headers, environment variables, error handling, and caching can be controlled.

Be careful

Frontend JavaScript

If an API uses a secret key, putting that key in static HTML or browser JavaScript would expose it to anyone who views the page source.

Future improvements

The first version was intentionally simple: run the script with a ZIP code or coordinates, fetch the forecast, print the next few forecast periods, and exit. That version proved the API workflow worked before I added a more advanced interactive menu, saved settings, and repeatable terminal-app behavior.

Simple upgrades

Make it nicer

  • Save a default ZIP code in a small config file.
  • Add a --short mode that prints one-line summaries.
  • Add a --hourly option using the hourly forecast URL.
  • Cache the forecast for a short time to avoid repeated requests.
Project upgrades

Connect it to other work

  • Use it on a Raspberry Pi dashboard.
  • Add weather alerts for server room or garden checks.
  • Generate a static JSON file for a website.
  • Turn it into a Flask route later if a web UI is useful.
Related guide: this pairs with the API guide because it turns the API concept into a small working project.

What I learned

This project is small enough to understand quickly, but it still teaches real API and Linux habits.

APIs

Follow the data

The first API response may not be the final data. It may contain the next endpoint the program should request.

Python

Small scripts scale

A small script can start as a terminal tool, then later become part of a web app, dashboard, cron job, or automation workflow.

Linux

Tools should be runnable

A command-line app with arguments, errors, and clear output is easier to reuse than code that only works once inside an editor.

Final idea

This is the kind of small project that makes APIs click. The app asks Weather.gov for structured data, follows the forecast link, extracts the next forecast periods, and prints something useful in the terminal. It is simple, but it connects Python, APIs, JSON, HTTP headers, Linux commands, and real-world automation.