At the airport

Runnable examples for airport-level weather: METAR and TAF at departure, arrival, and alternates. For en-route bbox queries (Winds Aloft, PIREPs, AIRMETs), see En route.

#QuestionData used
1What is the current flight category at major US airports right now?METAR
2Does density altitude exceed runway elevation by your threshold?METAR
3What will the weather be when I depart (next few hours)?TAF
4New York → Los Angeles - weather at takeoff and landing, now vs forecast?METAR + TAF
5Current METAR summary at selected international airportsMETAR
6Does the current METAR pass a custom wind/visibility/ceiling checklist?METAR

Requirements

Auth
x-api-key on every request (direct subscription). Learn more →

Install

The scripts below use requests for API calls, rich for terminal tables, and plotly + pandas for the flight-category map. Install once:

pip install requests rich plotly pandas

1. Current flight category map (live METAR)

Question: At four major US airports, what is the current flight category from METAR?

METAR returns a flight category (VFR / MVFR / IFR / LIFR). This script plots each airport on a US map with color-coded labels from the flight category table.

import os
import webbrowser
from pathlib import Path

import pandas as pd
import plotly.express as px
import requests

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

# Demo airports: code → (latitude, longitude, city name)
AIRPORTS = {
    "KJFK": (40.6413, -73.7781, "New York (JFK)"),
    "KLAX": (33.9425, -118.4081, "Los Angeles"),
    "KORD": (41.9742, -87.9073, "Chicago"),
    "KDEN": (39.8561, -104.6737, "Denver"),
}

# Flight category → color + label (see METAR reference)
CONDITIONS = {
    "VFR":  ("#22c55e", "VFR - visual flight rules"),
    "MVFR": ("#3b82f6", "MVFR - marginal VFR"),
    "IFR":  ("#ef4444", "IFR - instrument flight rules"),
    "LIFR": ("#a855f7", "LIFR - low IFR"),
}


def fetch_metar(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def build_map() -> Path:
    rows = []
    for icao, (lat, lon, city) in AIRPORTS.items():
        metar = fetch_metar(icao)
        if not metar:
            continue
        p = metar["parsed"]
        code = p["flight_rules"]
        _color, label = CONDITIONS.get(code, ("#6b7280", code))
        wind = p["wind"]["speed"]
        rows.append({
            "city": city,
            "code": icao,
            "lat": lat,
            "lon": lon,
            "rating_code": code,
            "conditions": label,
            "wind": f"{wind} kt (~{round(wind * 1.15)} mph, ~{round(wind * 1.852)} km/h)",
            "map_label": f"{city}: {label}",
        })

    df = pd.DataFrame(rows)
    color_map = {k: v[0] for k, v in CONDITIONS.items()}

    fig = px.scatter_geo(
        df,
        lat="lat",
        lon="lon",
        color="rating_code",
        color_discrete_map=color_map,
        hover_name="city",
        hover_data={
            "conditions": True,
            "wind": True,
            "code": True,
            "rating_code": False,
            "lat": False,
            "lon": False,
            "map_label": False,
        },
        scope="usa",
        title="Current flight category by airport (METAR)",
        labels={"rating_code": "API code"},
    )
    fig.update_traces(
        marker={"size": 18, "line": {"width": 2, "color": "white"}},
        text=df["map_label"],
        textposition="top center",
    )
    fig.add_annotation(
        text="Green = VFR  |  Blue = MVFR  |  Red/magenta = IFR/LIFR",
        xref="paper", yref="paper", x=0.5, y=-0.08, showarrow=False,
        font={"size": 12},
    )

    out = Path("weather_map.html")
    fig.write_html(out, include_plotlyjs="cdn")
    return out


if __name__ == "__main__":
    path = build_map()
    print(f"Saved {path} - opening interactive map in your browser.")
    print("Hover a marker for wind and category. Colors follow METAR flight_rules.")
    webbrowser.open(path.resolve().as_uri())

Output: HTML map with four US airports, colored by flight_rules, with hover data for wind and category label.


2. Density altitude vs runway elevation

Question: At this airport, does parsed.density_altitude exceed runway elevation by more than your threshold?

METAR includes density altitude (how thin the air feels). Compare it to the runway elevation: a big gap means longer takeoff roll.

import os

import requests
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"


def fetch_metar(icao: str) -> dict:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    r.raise_for_status()
    return r.json()


def takeoff_performance_report(
    icao: str,
    runway_elevation_ft: int,
    concern_if_da_above_runway_ft: int = 1000,
) -> None:
    metar = fetch_metar(icao)
    name = metar.get("airport_name", icao)
    p = metar["parsed"]
    da = p["density_altitude"]
    gap = da - runway_elevation_ft
    hot_day = gap >= concern_if_da_above_runway_ft

    table = Table(title=f"Takeoff air density - {name} ({icao})")
    table.add_column("What", style="cyan")
    table.add_column("Value", justify="right")
    table.add_row("Runway elevation", f"{runway_elevation_ft} ft above sea level")
    table.add_row("Air feels like elevation", f"{da} ft (density altitude)")
    table.add_row("Difference", f"+{gap} ft - air is thinner than runway height suggests")
    table.add_row("Outside temperature", f"{p['temperature']} C / {round(p['temperature'] * 9/5 + 32)} F")
    table.add_row("Barometer", f"{p['altimeter']} inHg")
    console.print(table)

    if hot_day:
        msg = (
            f"The air is significantly thinner than normal for this runway ({gap} ft gap). "
            "Planes need more runway to lift off and climb slower. "
            "Check your aircraft handbook before assuming normal takeoff distance."
        )
        console.print(Panel(msg, title="WARN: possible reduced takeoff performance", style="red"))
    else:
        msg = (
            f"Density altitude is within {concern_if_da_above_runway_ft} ft of runway elevation. "
            "Takeoff performance is likely close to normal (still verify for your aircraft)."
        )
        console.print(Panel(msg, title="OK: no major thin-air warning for this threshold", style="green"))


if __name__ == "__main__":
    # JFK runway is almost at sea level (~13 ft)
    takeoff_performance_report("KJFK", runway_elevation_ft=13)

Output: Table comparing runway elevation, density altitude, and temperature; panel flags when the gap exceeds the threshold.


3. What will the weather be when I depart?

Question: I plan to take off from Chicago at the current UTC time - what conditions should I expect?

TAF is a timeline of forecast blocks. Most likely = the main FROM block; possible short spells = overlapping TEMPO / BECMG rows.

import os
from datetime import datetime, timezone

import requests
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

CONDITIONS = {
    "VFR":  "VFR - visual flight rules",
    "MVFR": "MVFR - marginal VFR",
    "IFR":  "IFR - instrument flight rules",
    "LIFR": "LIFR - low IFR",
}

LAYER_MEANING = {
    "FROM":  "Most likely for this time window",
    "TEMPO": "Possible brief worse spell (may not happen)",
    "BECMG": "Conditions gradually changing toward",
}


def parse_time(iso: str) -> datetime:
    return datetime.fromisoformat(iso.replace("Z", "+00:00"))


def fetch_taf(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/taf/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def print_departure_forecast(icao: str, departure: datetime) -> None:
    taf = fetch_taf(icao)
    if not taf:
        console.print(f"[red]No forecast (TAF) published for {icao} - common at small airfields.[/]")
        return

    name = taf.get("airport_name", icao)
    console.print(Panel(
        f"Forecast airport: {name} ({icao})\n"
        f"Departure time (UTC): {departure.strftime('%Y-%m-%d %H:%M')} Z\n"
        f"Below = parsed TAF layers active at that instant.",
        title="Question: forecast at departure time",
    ))

    forecast = taf["parsed"]["forecast"]
    baseline = next(
        (
            p for p in forecast
            if p["type"] == "FROM"
            and parse_time(p["start_time"]) <= departure <= parse_time(p["end_time"])
        ),
        None,
    )
    modifiers = [
        p for p in forecast
        if p is not baseline
        and parse_time(p["start_time"]) <= departure <= parse_time(p["end_time"])
    ]

    table = Table(title="Forecast layers at your departure time")
    table.add_column("Role", style="cyan")
    table.add_column("Conditions")
    table.add_column("Weather details")

    if baseline:
        code = baseline["flight_rules"]
        wx = ", ".join(w["repr"] for w in baseline["wx_codes"]) or "No significant weather"
        table.add_row(
            LAYER_MEANING.get("FROM", "Most likely"),
            CONDITIONS.get(code, code),
            wx,
        )
    else:
        table.add_row("Most likely", "-", "No matching forecast block for this time")

    for p in modifiers:
        code = p["flight_rules"]
        wx = ", ".join(w["repr"] for w in p["wx_codes"]) or "-"
        prob = (p.get("probability") or {}).get("value")
        role = LAYER_MEANING.get(p["type"], p["type"])
        if prob:
            role += f" ({prob}% chance)"
        table.add_row(role, CONDITIONS.get(code, code), wx)

    console.print(table)
    console.print(
        "\n[dim]Show the baseline FROM row and any overlapping TEMPO/BECMG rows separately.[/dim]"
    )


if __name__ == "__main__":
    print_departure_forecast("KORD", datetime.now(timezone.utc))

Output: Departure-time panel plus a table with the active FROM row and overlapping modifier periods.


4. New York → Los Angeles - weather at both ends?

Question: I'm flying KJFK → KLAX - what's the weather at departure and arrival right now, and what's the worst forecast in the next ~24 hours?

import os
import concurrent.futures

import requests
from rich.console import Console
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

CONDITIONS = {
    "VFR":  "VFR - visual flight rules",
    "MVFR": "MVFR - marginal VFR",
    "IFR":  "IFR - instrument flight rules",
    "LIFR": "LIFR - low IFR",
}
SEVERITY = {"VFR": 0, "MVFR": 1, "IFR": 2, "LIFR": 3}
STYLE = {"VFR": "green", "MVFR": "blue", "IFR": "red", "LIFR": "magenta"}


def fetch_metar(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def fetch_taf(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/taf/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def worst_forecast_label(taf: dict | None) -> str:
    if not taf:
        return "No forecast available"
    from_blocks = [p for p in taf["parsed"]["forecast"] if p["type"] == "FROM"]
    if not from_blocks:
        return "No forecast available"
    worst = max(from_blocks, key=lambda p: SEVERITY.get(p["flight_rules"], -1))
    code = worst["flight_rules"]
    return CONDITIONS.get(code, code)


def print_route_weather(dep_icao: str, arr_icao: str) -> None:
    with concurrent.futures.ThreadPoolExecutor() as ex:
        metar_dep = ex.submit(fetch_metar, dep_icao).result()
        taf_dep = ex.submit(fetch_taf, dep_icao).result()
        metar_arr = ex.submit(fetch_metar, arr_icao).result()
        taf_arr = ex.submit(fetch_taf, arr_icao).result()

    table = Table(title=f"Route weather: {dep_icao} (departure) -> {arr_icao} (arrival)")
    table.add_column("Airport", style="cyan")
    table.add_column("Weather RIGHT NOW", max_width=36)
    table.add_column("Worst forecast in next ~24 h", max_width=36)

    for icao, metar, taf in [
        (dep_icao, metar_dep, taf_dep),
        (arr_icao, metar_arr, taf_arr),
    ]:
        if metar:
            code = metar["parsed"]["flight_rules"]
            now = CONDITIONS.get(code, code)
            style = STYLE.get(code, "white")
            airport = metar.get("airport_name", icao)
        else:
            now, style, airport = "No observation", "white", icao

        table.add_row(
            f"{airport}\n({icao})",
            f"[{style}]{now}[/]",
            worst_forecast_label(taf),
        )

    console.print(table)
    console.print(
        "\n[dim]RIGHT NOW = live METAR observation. "
        "Worst forecast = poorest 'most likely' TAF block in the next day.[/dim]"
    )


if __name__ == "__main__":
    print_route_weather("KJFK", "KLAX")

Output: Two rows (departure and arrival): current flight_rules from METAR and worst FROM category from TAF.


5. Multi-airport METAR dashboard

Question: Fetch parsed METAR for a fixed list of airports and print wind, temperature, visibility, and flight_rules.

import os

import requests
from rich.console import Console
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

AIRPORTS = ["KJFK", "KLAX", "EGLL", "YSSY"]  # New York, LA, London, Sydney

CONDITIONS = {
    "VFR":  "VFR - visual flight rules",
    "MVFR": "MVFR - marginal VFR",
    "IFR":  "IFR - instrument flight rules",
    "LIFR": "LIFR - low IFR",
}
STYLE = {"VFR": "green", "MVFR": "blue", "IFR": "red", "LIFR": "magenta"}


def fetch_metar(icao: str) -> dict | None:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()


def visibility_text(parsed: dict) -> str:
    v = parsed["visibility"]["value"]
    repr_ = parsed["visibility"]["repr"]
    if v is None:
        return "More than 6 miles visibility"
    # Some international stations return coded values (e.g. 9999 = 10 km+) - not statute miles.
    if v > 50 or repr_ in ("9999", "P6"):
        return f"{repr_} (coded - check raw METAR for units)"
    return f"{v} miles visibility"


def print_live_dashboard(airports: list[str]) -> None:
    table = Table(title="Live airport weather (METAR - updates every 30–60 min)")
    table.add_column("Airport", style="cyan", max_width=28)
    table.add_column("Flight category", max_width=30)
    table.add_column("Wind")
    table.add_column("Temp")
    table.add_column("Visibility")

    for icao in airports:
        data = fetch_metar(icao)
        if not data:
            table.add_row(icao, "No data", "-", "-", "-")
            continue
        p = data["parsed"]
        code = p["flight_rules"]
        label = CONDITIONS.get(code, code)
        style = STYLE.get(code, "white")
        gust = p["wind"]["gust"]
        wind = f"{p['wind']['speed']} kt"
        if gust:
            wind += f" (gusts {gust} kt)"
        table.add_row(
            data["airport_name"],
            f"[{style}]{label}[/]",
            wind,
            f"{p['temperature']} C",
            visibility_text(p),
        )

    console.print(table)
    console.print(
        "\n[dim]In a real dashboard, refresh every 5 minutes - "
        "that is often enough and saves API quota.[/dim]"
    )


if __name__ == "__main__":
    print_live_dashboard(AIRPORTS)

Output: Terminal table with airport name, flight_rules label, wind, temperature, and visibility.


6. Personal limits checklist (demo)

Question: Given configurable wind, gust, visibility, and ceiling limits, does the current METAR pass?

Not a substitute for an official weather briefing or regulatory minimums.

import os

import requests
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

console = Console()

HEADERS = {
    "X-RapidAPI-Key": os.getenv("RAPIDAPI_KEY", "YOUR_RAPIDAPI_KEY"),
    "X-RapidAPI-Host": "skylink-api.p.rapidapi.com",
}
BASE = "https://skylink-api.p.rapidapi.com"

# Demo personal limits - change these to match your skills / aircraft
MY_LIMITS = {
    "max_wind_kt": 25,
    "max_gust_kt": 35,
    "min_visibility_sm": 1.0,
    "min_ceiling_ft": 500,
}


def fetch_metar(icao: str) -> dict:
    r = requests.get(
        f"{BASE}/weather/metar/{icao}",
        headers=HEADERS,
        params={"parsed": "true"},
        timeout=(10, 15),
    )
    r.raise_for_status()
    return r.json()


def print_safety_check(icao: str) -> None:
    metar = fetch_metar(icao)
    name = metar.get("airport_name", icao)
    p = metar["parsed"]
    code = p["flight_rules"]

    limits = Table(title=f"Your demo limits (edit MY_LIMITS in the script)")
    limits.add_column("Rule")
    limits.add_column("Your limit", justify="right")
    limits.add_row("Max steady wind", f"{MY_LIMITS['max_wind_kt']} kt")
    limits.add_row("Max gusts", f"{MY_LIMITS['max_gust_kt']} kt")
    limits.add_row("Min visibility", f"{MY_LIMITS['min_visibility_sm']} miles")
    limits.add_row("Min cloud ceiling", f"{MY_LIMITS['min_ceiling_ft']} ft")
    console.print(limits)

    problems: list[str] = []

    wind = p["wind"]["speed"]
    if wind > MY_LIMITS["max_wind_kt"]:
        problems.append(f"Wind too strong: {wind} kt (your max {MY_LIMITS['max_wind_kt']} kt)")

    gust = p["wind"]["gust"]
    if gust and gust > MY_LIMITS["max_gust_kt"]:
        problems.append(f"Gusts too strong: {gust} kt (your max {MY_LIMITS['max_gust_kt']} kt)")

    vis = p["visibility"]["value"]
    if vis is not None and vis < MY_LIMITS["min_visibility_sm"]:
        problems.append(
            f"Visibility too low: {vis} miles (you need at least {MY_LIMITS['min_visibility_sm']} mi)"
        )

    ceiling_layers = [c for c in p["clouds"] if c["type"] in ("BKN", "OVC")]
    if ceiling_layers:
        ceiling_ft = ceiling_layers[0]["base"] * 100
        if ceiling_ft < MY_LIMITS["min_ceiling_ft"]:
            problems.append(
                f"Clouds too low: base at {ceiling_ft} ft (you need at least {MY_LIMITS['min_ceiling_ft']} ft)"
            )

    safe = len(problems) == 0
    if safe:
        body = (
            f"All demo checks passed at {name}.\n"
            f"Current overall rating from METAR: {code}.\n"
            "This does NOT replace a full weather briefing."
        )
        title = f"Within demo limits - {icao}"
        style = "green"
    else:
        body = "Problems found:\n" + "\n".join(f"- {x}" for x in problems)
        title = f"Exceeds demo limits - {icao}"
        style = "red"

    console.print(Panel(body, title=title, style=style))


if __name__ == "__main__":
    print_safety_check("KJFK")

Output: Limits table, then a panel listing passed checks or each failed rule against MY_LIMITS.


En-route weather (bbox endpoints)

Questions 7–9 query a bounding box along your route. Build the box from departure and arrival coordinates (plus a buffer), or from waypoints. Empty reports / total: 0 is normal when no PIREPs were filed or no advisories are active - not an API failure.

Helper used below:

def route_bbox(
    dep_lat: float, dep_lon: float,
    arr_lat: float, arr_lon: float,
    pad_deg: float = 2.0,
) -> str:
    """SW corner first, then NE - matches API bbox format."""
    lat1 = min(dep_lat, arr_lat) - pad_deg
    lat2 = max(dep_lat, arr_lat) + pad_deg
    lon1 = min(dep_lon, arr_lon) - pad_deg
    lon2 = max(dep_lon, arr_lon) + pad_deg
    return f"{lat1},{lon1},{lat2},{lon2}"

Coordinates for KJFK (40.6413, -73.7781) and KLAX (33.9425, -118.4081).