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