Python

【Python】WebAPIを取得する「お天気アプリ」を作ってみた

takahide

おはようございます。タカヒデです。

本日はPythonで「WebAPIを取得するお天気アプリ」を作ってみました。

STEP形式で解説しているので、「まずは何かを作ってみたい」という初心者の方の参考になれば幸いです。

こんな人にオススメ
  • プログラミング初心者
  • 何から始めればよいか分からない
  • APIでデータを取得する簡単なアプリを作ってみたい

ぜひ実際にコードを打ちながら作成してみてください。

「お天気アプリ」完成イメージ

まずは、「お天気アプリ」完成後の最終的なコードと完成イメージです。

import json
import threading
import urllib.parse
import urllib.request
from dataclasses import dataclass
from datetime import datetime
from typing import Callable, Optional

import tkinter as tk
from tkinter import ttk


# -----------------------------
# Configuration
# -----------------------------
APP_TITLE = "World Weather Now"
APP_BG = "#0a1020"
CARD_BG = "#111b2e"

CITIES = [
    ("Tokyo", 35.6762, 139.6503),
    ("Osaka", 34.6937, 135.5023),
    ("Singapore", 1.3521, 103.8198),
    ("London", 51.5074, -0.1278),
    ("Paris", 48.8566, 2.3522),
    ("Berlin", 52.5200, 13.4050),
    ("New York", 40.7128, -74.0060),
    ("Los Angeles", 34.0522, -118.2437),
    ("Dubai", 25.2048, 55.2708),
]


# -----------------------------
# Domain
# -----------------------------
def weather_text(code: int) -> str:
    if code == 0:
        return "Sunny"
    if code in (1, 2):
        return "Mostly Sunny"
    if code == 3:
        return "Cloudy"
    if code in (45, 48):
        return "Fog"
    if code in (51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82):
        return "Rain"
    if code in (71, 73, 75, 77, 85, 86):
        return "Snow"
    if code in (95, 96, 99):
        return "Thunder"
    return "Unknown"


@dataclass(frozen=True)
class City:
    name: str
    lat: float
    lon: float


@dataclass(frozen=True)
class CurrentWeather:
    temperature: float
    weathercode: int
    time_iso: str

    @property
    def observed_hhmm(self) -> str:
        try:
            return datetime.fromisoformat(self.time_iso).strftime("%H:%M")
        except Exception:
            return "--:--"


# -----------------------------
# Data access
# -----------------------------
class OpenMeteoClient:
    def __init__(self, timeout_sec: int = 10):
        self.timeout_sec = timeout_sec

    def fetch_current(self, city: City) -> CurrentWeather:
        base_url = "https://api.open-meteo.com/v1/forecast"
        params = {
            "latitude": f"{city.lat:.4f}",
            "longitude": f"{city.lon:.4f}",
            "current_weather": "true",
            "timezone": "auto",
        }
        url = base_url + "?" + urllib.parse.urlencode(params)

        with urllib.request.urlopen(url, timeout=self.timeout_sec) as resp:
            data = json.loads(resp.read().decode("utf-8"))

        current = data["current_weather"]
        return CurrentWeather(
            temperature=float(current["temperature"]),
            weathercode=int(current["weathercode"]),
            time_iso=str(current["time"]),
        )


# -----------------------------
# UI components
# -----------------------------
class Styles:
    def __init__(self, style: ttk.Style):
        self._style = style

    def apply(self) -> None:
        try:
            self._style.theme_use("clam")
        except Exception:
            pass

        self._style.configure("Card.TFrame", background=CARD_BG, padding=14)
        self._style.configure(
            "City.TLabel",
            background=CARD_BG,
            foreground="#e7eefc",
            font=("Segoe UI", 12, "bold"),
        )
        self._style.configure(
            "Weather.TLabel",
            background=CARD_BG,
            foreground="#c6d1ea",
            font=("Segoe UI", 16, "bold"),
        )
        self._style.configure(
            "Temp.TLabel",
            background=CARD_BG,
            foreground="#e7eefc",
            font=("Segoe UI", 18, "bold"),
        )
        self._style.configure(
            "Time.TLabel",
            background=CARD_BG,
            foreground="#9fb0d0",
            font=("Segoe UI", 9),
        )


class CityCard(ttk.Frame):
    def __init__(self, master: tk.Misc, city_name: str):
        super().__init__(master, style="Card.TFrame")
        self._city_name = city_name

        self._lbl_city = ttk.Label(self, text=city_name, style="City.TLabel")
        self._lbl_city.grid(row=0, column=0, sticky="w")

        self._lbl_weather = ttk.Label(self, text="--", style="Weather.TLabel")
        self._lbl_weather.grid(row=1, column=0, sticky="w", pady=(6, 0))

        self._lbl_temp = ttk.Label(self, text="--.- °C", style="Temp.TLabel")
        self._lbl_temp.grid(row=0, column=1, rowspan=2, sticky="e")

        self._lbl_time = ttk.Label(self, text="--:--", style="Time.TLabel")
        self._lbl_time.grid(row=2, column=0, columnspan=2, sticky="w", pady=(6, 0))

        self.grid_columnconfigure(0, weight=1)

    def set_loading(self) -> None:
        self._lbl_weather.config(text="Loading")
        self._lbl_temp.config(text="...")
        self._lbl_time.config(text="")

    def set_error(self) -> None:
        self._lbl_weather.config(text="Error")
        self._lbl_temp.config(text="--")
        self._lbl_time.config(text="")

    def set_weather(self, cw: CurrentWeather) -> None:
        self._lbl_weather.config(text=weather_text(cw.weathercode))
        self._lbl_temp.config(text=f"{cw.temperature:.1f} °C")
        self._lbl_time.config(text=f"Observed {cw.observed_hhmm}")


# -----------------------------
# App
# -----------------------------
class WeatherApp(tk.Tk):
    def __init__(
        self,
        cities: list[City],
        client: OpenMeteoClient,
        now_provider: Callable[[], datetime] = datetime.now,
    ):
        super().__init__()
        self._cities = cities
        self._client = client
        self._now_provider = now_provider

        self.title(APP_TITLE)
        self.geometry("640x720")
        self.configure(bg=APP_BG)

        Styles(ttk.Style(self)).apply()

        self._build_header()
        self._build_body()

        self._tick_clock()
        self._refresh_async()

    def _build_header(self) -> None:
        header = tk.Frame(self, bg=APP_BG)
        header.pack(fill="x", padx=16, pady=(16, 8))

        tk.Label(
            header,
            text=APP_TITLE,
            fg="#e7eefc",
            bg=APP_BG,
            font=("Segoe UI", 18, "bold"),
        ).pack(side="left")

        self._lbl_now = tk.Label(
            header,
            text="",
            fg="#9fb0d0",
            bg=APP_BG,
            font=("Segoe UI", 10),
        )
        self._lbl_now.pack(side="right")

    def _build_body(self) -> None:
        body = tk.Frame(self, bg=APP_BG)
        body.pack(fill="both", expand=True, padx=16, pady=8)

        self._cards: dict[str, CityCard] = {}
        for city in self._cities:
            card = CityCard(body, city.name)
            card.pack(fill="x", pady=8)
            self._cards[city.name] = card

    def _tick_clock(self) -> None:
        self._lbl_now.config(text=f"Now {self._now_provider().strftime('%H:%M:%S')}")
        self.after(500, self._tick_clock)

    def _refresh_async(self) -> None:
        for card in self._cards.values():
            card.set_loading()

        threading.Thread(target=self._worker_fetch_all, daemon=True).start()

    def _worker_fetch_all(self) -> None:
        for city in self._cities:
            card = self._cards.get(city.name)
            if card is None:
                continue

            try:
                cw = self._client.fetch_current(city)
                self.after(0, card.set_weather, cw)
            except Exception:
                self.after(0, card.set_error)


def main() -> None:
    cities = [City(name, lat, lon) for name, lat, lon in CITIES]
    app = WeatherApp(cities=cities, client=OpenMeteoClient(timeout_sec=10))
    app.mainloop()


if __name__ == "__main__":
    main()

アプリを起動すると、世界の主要都市について、現在の天気と気温が一覧で表示されます。

画面右上には現在時刻が表示され、起動時に自動で最新の天気情報を取得します。

なお、本記事で使用するAPIは「Open-Meteo」というオープンソースの天気APIです。
商用でなければ無料で利用可能でき、会員登録も不要なので、学習にオススメです。

STEP1:ウィンドウを作る

STEP1では、「アプリの土台となるウィンドウ」を作ります。
加えて、ウィンドウのタイトルやサイズ、背景色、時刻表示エリアのみを用意します。

import tkinter as tk

APP_TITLE = "World Weather Now"
APP_BG = "#0a1020"


class WeatherApp(tk.Tk):
    def __init__(self):
        super().__init__()

        self.title(APP_TITLE)
        self.geometry("640x720")
        self.configure(bg=APP_BG)

        self._build_header()

    def _build_header(self) -> None:
        header = tk.Frame(self, bg=APP_BG)
        header.pack(fill="x", padx=16, pady=(16, 8))

        tk.Label(
            header,
            text=APP_TITLE,
            fg="#e7eefc",
            bg=APP_BG,
            font=("Segoe UI", 18, "bold"),
        ).pack(side="left")

        self._lbl_now = tk.Label(
            header,
            text="Now --:--:--",
            fg="#9fb0d0",
            bg=APP_BG,
            font=("Segoe UI", 10),
        )
        self._lbl_now.pack(side="right")


def main() -> None:
    app = WeatherApp()
    app.mainloop()


if __name__ == "__main__":
    main()

手順①:Tkinterでウィンドウを作る

class WeatherApp(tk.Tk):
    def __init__(self):
        super().__init__()

        self.title(APP_TITLE)
        self.geometry("640x720")
        self.configure(bg=APP_BG)

今回のアプリは「Tkinter」という、Python 標準の GUI ライブラリで作ります。

  • self.title(APP_TITLE)
    • ウィンドウタイトルを指定
  • self.geometry(“640×720”)
    • ウィンドウサイズを指定
  • self.configure(bg=APP_BG)
    • 背景色を指定

手順②:ヘッダーを作る

    def _build_header(self) -> None:
        header = tk.Frame(self, bg=APP_BG)
        header.pack(fill="x", padx=16, pady=(16, 8))

「_build_header() 」では、上部にヘッダー領域を作っています。

  • tk.Frame
    • 部品をまとめる箱
    • tk.Labelを置いて、左にタイトル、右に時刻表示エリアを並べる

この段階では、まだ時刻は動かしていません。
現時点では以下のアプリができています。

STEP2:都市カードを表示する

STEP2では、都市ごとのカードを並べて、天気表示の土台を作ります。
まだ天気情報は取得しないので、各カードには仮の表示を入れておきます。

import tkinter as tk
# ★STEP2追加
from tkinter import ttk


APP_TITLE = "World Weather Now"
APP_BG = "#0a1020"
# ★STEP2追加
CARD_BG = "#111b2e"
CITIES = [
    ("Tokyo", 35.6762, 139.6503),
    ("Osaka", 34.6937, 135.5023),
    ("Singapore", 1.3521, 103.8198),
    ("London", 51.5074, -0.1278),
    ("Paris", 48.8566, 2.3522),
    ("Berlin", 52.5200, 13.4050),
    ("New York", 40.7128, -74.0060),
    ("Los Angeles", 34.0522, -118.2437),
    ("Dubai", 25.2048, 55.2708),
]


# ★STEP2追加
class Styles:
    def __init__(self, style: ttk.Style):
        self._style = style

    def apply(self) -> None:
        try:
            self._style.theme_use("clam")
        except Exception:
            pass

        self._style.configure("Card.TFrame", background=CARD_BG, padding=14)
        self._style.configure(
            "City.TLabel",
            background=CARD_BG,
            foreground="#e7eefc",
            font=("Segoe UI", 12, "bold"),
        )
        self._style.configure(
            "Weather.TLabel",
            background=CARD_BG,
            foreground="#c6d1ea",
            font=("Segoe UI", 16, "bold"),
        )
        self._style.configure(
            "Temp.TLabel",
            background=CARD_BG,
            foreground="#e7eefc",
            font=("Segoe UI", 18, "bold"),
        )
        self._style.configure(
            "Time.TLabel",
            background=CARD_BG,
            foreground="#9fb0d0",
            font=("Segoe UI", 9),
        )


# ★STEP2追加
class CityCard(ttk.Frame):
    def __init__(self, master: tk.Misc, city_name: str):
        super().__init__(master, style="Card.TFrame")

        self._lbl_city = ttk.Label(self, text=city_name, style="City.TLabel")
        self._lbl_city.grid(row=0, column=0, sticky="w")

        self._lbl_weather = ttk.Label(self, text="Loading", style="Weather.TLabel")
        self._lbl_weather.grid(row=1, column=0, sticky="w", pady=(6, 0))

        self._lbl_temp = ttk.Label(self, text="...", style="Temp.TLabel")
        self._lbl_temp.grid(row=0, column=1, rowspan=2, sticky="e")

        self._lbl_time = ttk.Label(self, text="", style="Time.TLabel")
        self._lbl_time.grid(row=2, column=0, columnspan=2, sticky="w", pady=(6, 0))

        self.grid_columnconfigure(0, weight=1)


class WeatherApp(tk.Tk):
    def __init__(self):
        super().__init__()

        self.title(APP_TITLE)
        self.geometry("640x720")
        self.configure(bg=APP_BG)

        # ★STEP2追加
        Styles(ttk.Style(self)).apply()

        self._build_header()
        # ★STEP2追加
        self._build_body()

    def _build_header(self) -> None:
        header = tk.Frame(self, bg=APP_BG)
        header.pack(fill="x", padx=16, pady=(16, 8))

        tk.Label(
            header,
            text=APP_TITLE,
            fg="#e7eefc",
            bg=APP_BG,
            font=("Segoe UI", 18, "bold"),
        ).pack(side="left")

        self._lbl_now = tk.Label(
            header,
            text="Now --:--:--",
            fg="#9fb0d0",
            bg=APP_BG,
            font=("Segoe UI", 10),
        )
        self._lbl_now.pack(side="right")

    # ★STEP2追加
    def _build_body(self) -> None:
        body = tk.Frame(self, bg=APP_BG)
        body.pack(fill="both", expand=True, padx=16, pady=8)

        self._cards: dict[str, CityCard] = {}
        for name, _, _ in CITIES:
            card = CityCard(body, name)
            card.pack(fill="x", pady=8)
            self._cards[name] = card


def main() -> None:
    app = WeatherApp()
    app.mainloop()


if __name__ == "__main__":
    main()

手順①:ttkとスタイルを使ってカードの見た目を作る

from tkinter import ttk

Tkinterには、見た目が少し整った部品セットとしてttkが用意されています。
「カードの枠」「フォントを揃えたラベル」に使うことで整理しやすくなります。

またStylesクラスでは、「ttk.Style」を使って以下をまとめて設定しています。

  • カードの背景色
  • 文字色
  • フォントサイズ

手順②:各都市の座標を設定する

CITIES = [
    ("Tokyo", 35.6762, 139.6503),
    ("Osaka", 34.6937, 135.5023),
    ("Singapore", 1.3521, 103.8198),
    ("London", 51.5074, -0.1278),
    ("Paris", 48.8566, 2.3522),
    ("Berlin", 52.5200, 13.4050),
    ("New York", 40.7128, -74.0060),
    ("Los Angeles", 34.0522, -118.2437),
    ("Dubai", 25.2048, 55.2708),
]

Open-Meteoの APIは、都市名ではなく緯度と経度を指定して天気を返す仕組みになっています。

そのため、必要な年の座標を自分で調べて固定値として記載しています。
都市を追加したい場合は、Google検索などで事前に調べて記載しましょう。

手順③:都市カードを並べて、表示の器を完成させる

for name, _, _ in CITIES:
    card = CityCard(body, name)
    card.pack(fill="x", pady=8)

ここでは、都市リストを順番に回してCityCardを作り、縦方向に並べています。

この段階では、まだ API から天気を取得していないので、カードの中は仮表示ですが、以下のアプリができているはずです。

STEP3:右上に現在時刻を表示する

STEP3では、ヘッダー右上の表示を、実際の現在時刻が更新される表示にします。

import tkinter as tk
from tkinter import ttk
# ★STEP3追加
from datetime import datetime


APP_TITLE = "World Weather Now"
APP_BG = "#0a1020"
CARD_BG = "#111b2e"

CITIES = [
    ("Tokyo", 35.6762, 139.6503),
    ("Osaka", 34.6937, 135.5023),
    ("Singapore", 1.3521, 103.8198),
    ("London", 51.5074, -0.1278),
    ("Paris", 48.8566, 2.3522),
    ("Berlin", 52.5200, 13.4050),
    ("New York", 40.7128, -74.0060),
    ("Los Angeles", 34.0522, -118.2437),
    ("Dubai", 25.2048, 55.2708),
]


class Styles:
    def __init__(self, style: ttk.Style):
        self._style = style

    def apply(self) -> None:
        try:
            self._style.theme_use("clam")
        except Exception:
            pass

        self._style.configure("Card.TFrame", background=CARD_BG, padding=14)
        self._style.configure(
            "City.TLabel",
            background=CARD_BG,
            foreground="#e7eefc",
            font=("Segoe UI", 12, "bold"),
        )
        self._style.configure(
            "Weather.TLabel",
            background=CARD_BG,
            foreground="#c6d1ea",
            font=("Segoe UI", 16, "bold"),
        )
        self._style.configure(
            "Temp.TLabel",
            background=CARD_BG,
            foreground="#e7eefc",
            font=("Segoe UI", 18, "bold"),
        )
        self._style.configure(
            "Time.TLabel",
            background=CARD_BG,
            foreground="#9fb0d0",
            font=("Segoe UI", 9),
        )


class CityCard(ttk.Frame):
    def __init__(self, master: tk.Misc, city_name: str):
        super().__init__(master, style="Card.TFrame")

        self._lbl_city = ttk.Label(self, text=city_name, style="City.TLabel")
        self._lbl_city.grid(row=0, column=0, sticky="w")

        self._lbl_weather = ttk.Label(self, text="Loading", style="Weather.TLabel")
        self._lbl_weather.grid(row=1, column=0, sticky="w", pady=(6, 0))

        self._lbl_temp = ttk.Label(self, text="...", style="Temp.TLabel")
        self._lbl_temp.grid(row=0, column=1, rowspan=2, sticky="e")

        self._lbl_time = ttk.Label(self, text="", style="Time.TLabel")
        self._lbl_time.grid(row=2, column=0, columnspan=2, sticky="w", pady=(6, 0))

        self.grid_columnconfigure(0, weight=1)


class WeatherApp(tk.Tk):
    def __init__(self):
        super().__init__()

        self.title(APP_TITLE)
        self.geometry("640x720")
        self.configure(bg=APP_BG)

        Styles(ttk.Style(self)).apply()

        self._build_header()
        self._build_body()

        # ★STEP3追加
        self._tick_clock()

    def _build_header(self) -> None:
        header = tk.Frame(self, bg=APP_BG)
        header.pack(fill="x", padx=16, pady=(16, 8))

        tk.Label(
            header,
            text=APP_TITLE,
            fg="#e7eefc",
            bg=APP_BG,
            font=("Segoe UI", 18, "bold"),
        ).pack(side="left")

        self._lbl_now = tk.Label(
            header,
            text="",
            fg="#9fb0d0",
            bg=APP_BG,
            font=("Segoe UI", 10),
        )
        self._lbl_now.pack(side="right")

    def _build_body(self) -> None:
        body = tk.Frame(self, bg=APP_BG)
        body.pack(fill="both", expand=True, padx=16, pady=8)

        self._cards: dict[str, CityCard] = {}
        for name, _, _ in CITIES:
            card = CityCard(body, name)
            card.pack(fill="x", pady=8)
            self._cards[name] = card

    # ★STEP3追加
    def _tick_clock(self) -> None:
        now_str = datetime.now().strftime("%H:%M:%S")
        self._lbl_now.config(text=f"Now {now_str}")
        self.after(500, self._tick_clock)


def main() -> None:
    app = WeatherApp()
    app.mainloop()


if __name__ == "__main__":
    main()

手順①:現在時刻を扱うためにdatetimeをインポートする

from datetime import datetime

Pythonで現在時刻を取り出すために、「datetime」をインポートします。

手順②:afterを使って時刻表示を定期的に更新する

def _tick_clock(self) -> None:
    now_str = datetime.now().strftime("%H:%M:%S")
    self._lbl_now.config(text=f"Now {now_str}")
    self.after(500, self._tick_clock)

「datetime.now()」で現在時刻を取得し、「strftime」で「時:分:秒」の形に整えています。
そして self.after(500, …)を使うことで、500ミリ秒ごとに同じ関数を呼び直し、表示を更新しています。

これでアプリ右上に現在時刻が表示されるようになりました。

STEP4:天気データを扱う準備をする

STEP4では、外部APIから取得する「天気データ」を、コードの中で扱いやすい形に整えます。
具体的には、天気コードを文字に変換する関数と、都市・天気の情報を表すクラスを用意します。

import tkinter as tk
from tkinter import ttk
from datetime import datetime
# ★STEP4追加
from dataclasses import dataclass


APP_TITLE = "World Weather Now"
APP_BG = "#0a1020"
CARD_BG = "#111b2e"

CITIES = [
    ("Tokyo", 35.6762, 139.6503),
    ("Osaka", 34.6937, 135.5023),
    ("Singapore", 1.3521, 103.8198),
    ("London", 51.5074, -0.1278),
    ("Paris", 48.8566, 2.3522),
    ("Berlin", 52.5200, 13.4050),
    ("New York", 40.7128, -74.0060),
    ("Los Angeles", 34.0522, -118.2437),
    ("Dubai", 25.2048, 55.2708),
]


# ★STEP4追加
def weather_text(code: int) -> str:
    if code == 0:
        return "Sunny"
    if code in (1, 2):
        return "Mostly Sunny"
    if code == 3:
        return "Cloudy"
    if code in (45, 48):
        return "Fog"
    if code in (51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82):
        return "Rain"
    if code in (71, 73, 75, 77, 85, 86):
        return "Snow"
    if code in (95, 96, 99):
        return "Thunder"
    return "Unknown"


# ★STEP4追加
@dataclass(frozen=True)
class City:
    name: str
    lat: float
    lon: float


# ★STEP4追加
@dataclass(frozen=True)
class CurrentWeather:
    temperature: float
    weathercode: int
    time_iso: str

    @property
    def observed_hhmm(self) -> str:
        try:
            return datetime.fromisoformat(self.time_iso).strftime("%H:%M")
        except Exception:
            return "--:--"


class Styles:
    def __init__(self, style: ttk.Style):
        self._style = style

    def apply(self) -> None:
        try:
            self._style.theme_use("clam")
        except Exception:
            pass

        self._style.configure("Card.TFrame", background=CARD_BG, padding=14)
        self._style.configure(
            "City.TLabel",
            background=CARD_BG,
            foreground="#e7eefc",
            font=("Segoe UI", 12, "bold"),
        )
        self._style.configure(
            "Weather.TLabel",
            background=CARD_BG,
            foreground="#c6d1ea",
            font=("Segoe UI", 16, "bold"),
        )
        self._style.configure(
            "Temp.TLabel",
            background=CARD_BG,
            foreground="#e7eefc",
            font=("Segoe UI", 18, "bold"),
        )
        self._style.configure(
            "Time.TLabel",
            background=CARD_BG,
            foreground="#9fb0d0",
            font=("Segoe UI", 9),
        )


class CityCard(ttk.Frame):
    def __init__(self, master: tk.Misc, city_name: str):
        super().__init__(master, style="Card.TFrame")

        self._lbl_city = ttk.Label(self, text=city_name, style="City.TLabel")
        self._lbl_city.grid(row=0, column=0, sticky="w")

        # ★STEP4変更
        self._lbl_weather = ttk.Label(self, text="--", style="Weather.TLabel")
        self._lbl_weather.grid(row=1, column=0, sticky="w", pady=(6, 0))

        # ★STEP4変更
        self._lbl_temp = ttk.Label(self, text="--.- °C", style="Temp.TLabel")
        self._lbl_temp.grid(row=0, column=1, rowspan=2, sticky="e")

        # ★STEP4変更
        self._lbl_time = ttk.Label(self, text="--:--", style="Time.TLabel")
        self._lbl_time.grid(row=2, column=0, columnspan=2, sticky="w", pady=(6, 0))

        self.grid_columnconfigure(0, weight=1)

    # ★STEP4追加
    def set_loading(self) -> None:
        self._lbl_weather.config(text="Loading")
        self._lbl_temp.config(text="...")
        self._lbl_time.config(text="")

    # ★STEP4追加
    def set_error(self) -> None:
        self._lbl_weather.config(text="Error")
        self._lbl_temp.config(text="--")
        self._lbl_time.config(text="")

    # ★STEP4追加
    def set_weather(self, cw: CurrentWeather) -> None:
        self._lbl_weather.config(text=weather_text(cw.weathercode))
        self._lbl_temp.config(text=f"{cw.temperature:.1f} °C")
        self._lbl_time.config(text=f"Observed {cw.observed_hhmm}")


class WeatherApp(tk.Tk):
    def __init__(self):
        super().__init__()

        self.title(APP_TITLE)
        self.geometry("640x720")
        self.configure(bg=APP_BG)

        Styles(ttk.Style(self)).apply()

        self._build_header()
        self._build_body()

        self._tick_clock()

    def _build_header(self) -> None:
        header = tk.Frame(self, bg=APP_BG)
        header.pack(fill="x", padx=16, pady=(16, 8))

        tk.Label(
            header,
            text=APP_TITLE,
            fg="#e7eefc",
            bg=APP_BG,
            font=("Segoe UI", 18, "bold"),
        ).pack(side="left")

        self._lbl_now = tk.Label(
            header,
            text="",
            fg="#9fb0d0",
            bg=APP_BG,
            font=("Segoe UI", 10),
        )
        self._lbl_now.pack(side="right")

    def _build_body(self) -> None:
        body = tk.Frame(self, bg=APP_BG)
        body.pack(fill="both", expand=True, padx=16, pady=8)

        self._cards: dict[str, CityCard] = {}
        for name, _, _ in CITIES:
            card = CityCard(body, name)
            card.pack(fill="x", pady=8)
            self._cards[name] = card

    def _tick_clock(self) -> None:
        now_str = datetime.now().strftime("%H:%M:%S")
        self._lbl_now.config(text=f"Now {now_str}")
        self.after(500, self._tick_clock)


def main() -> None:
    app = WeatherApp()
    app.mainloop()


if __name__ == "__main__":
    main()

手順①:dataclassのインポート

from dataclasses import dataclass

「@dataclass」を使えるようにインポートします。
これによって「都市」や「天気情報」を、バラバラの変数ではなく、まとまったデータのかたまりとして扱えるようにしています。

手順②:天気コードを「実際の天気」に変換する

def weather_text(code: int) -> str:
    if code == 0:
        return "Sunny"
    if code in (1, 2):
        return "Mostly Sunny"
    if code == 3:
        return "Cloudy"
    if code in (45, 48):
        return "Fog"
    if code in (51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82):
        return "Rain"
    if code in (71, 73, 75, 77, 85, 86):
        return "Snow"
    if code in (95, 96, 99):
        return "Thunder"
    return "Unknown"

Open-Meteoには「weathercode」という数値が入っています。
ただ、数値のままだと画面に出しても意味が分かりにくいので、ここで「Sunny」「Rain」などの文字に変換します。

手順③:データをクラスにまとめる

@dataclass(frozen=True)
class City:
    name: str
    lat: float
    lon: float

Cityは「都市名・緯度・経度」をひとまとめにしたクラスです。クラスにすると「何の値なのか」が読み取りやすくなります。

  • name:都市名
  • lat:緯度
  • lon:経度
class CurrentWeather:
    temperature: float
    weathercode: int
    time_iso: str

CurrentWeatherは API から得られる「気温・天気コード・観測時刻」をまとめたデータ型です。

  • temperature:気温
  • time_iso:観測時刻
  • weathercode:天気コード
    @property
    def observed_hhmm(self) -> str:
        try:
            return datetime.fromisoformat(self.time_iso).strftime("%H:%M")
        except Exception:
            return "--:--"

さらにobserved_hhmmというプロパティを用意しておくことで、時刻表示を、初期の「2025-12-21T09:15」から「09:15」に成形しています。

手順④:都市カードの表示を変更

 class CityCard(ttk.Frame):

都市カードの状態に「set_loading / set_error / set_weather」の3種類を追加しています。

  • set_loading()
    • データを取得中
  • set_error()
    • データ取得に失敗
  • set_weather(cw)
    • 「CurrentWeather」でデータを受け取って、ラベルに反映

以上がSTEP4です。
これで実行すると、大きな変更はありませんが、各都市に記載された内容が変わったことが分かります。

STEP5:Open-MeteoAPIから天気を取得する

このSTEPでは、Open-Meteo にアクセスして「現在の天気データ」を取得できるようにします。
まだ非同期処理は入れないので、取得処理は同期で書きます。

# ★STEP5追加
import json
import urllib.parse
import urllib.request

import tkinter as tk
from tkinter import ttk
from datetime import datetime
from dataclasses import dataclass


APP_TITLE = "World Weather Now"
APP_BG = "#0a1020"
CARD_BG = "#111b2e"

CITIES = [
    ("Tokyo", 35.6762, 139.6503),
    ("Osaka", 34.6937, 135.5023),
    ("Singapore", 1.3521, 103.8198),
    ("London", 51.5074, -0.1278),
    ("Paris", 48.8566, 2.3522),
    ("Berlin", 52.5200, 13.4050),
    ("New York", 40.7128, -74.0060),
    ("Los Angeles", 34.0522, -118.2437),
    ("Dubai", 25.2048, 55.2708),
]


def weather_text(code: int) -> str:
    if code == 0:
        return "Sunny"
    if code in (1, 2):
        return "Mostly Sunny"
    if code == 3:
        return "Cloudy"
    if code in (45, 48):
        return "Fog"
    if code in (51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82):
        return "Rain"
    if code in (71, 73, 75, 77, 85, 86):
        return "Snow"
    if code in (95, 96, 99):
        return "Thunder"
    return "Unknown"


@dataclass(frozen=True)
class City:
    name: str
    lat: float
    lon: float


@dataclass(frozen=True)
class CurrentWeather:
    temperature: float
    weathercode: int
    time_iso: str

    @property
    def observed_hhmm(self) -> str:
        try:
            return datetime.fromisoformat(self.time_iso).strftime("%H:%M")
        except Exception:
            return "--:--"


# ★STEP5追加
class OpenMeteoClient:
    def __init__(self, timeout_sec: int = 10):
        self.timeout_sec = timeout_sec

    def fetch_current(self, city: City) -> CurrentWeather:
        base_url = "https://api.open-meteo.com/v1/forecast"
        params = {
            "latitude": f"{city.lat:.4f}",
            "longitude": f"{city.lon:.4f}",
            "current_weather": "true",
            "timezone": "auto",
        }
        url = base_url + "?" + urllib.parse.urlencode(params)

        with urllib.request.urlopen(url, timeout=self.timeout_sec) as resp:
            data = json.loads(resp.read().decode("utf-8"))

        current = data["current_weather"]
        return CurrentWeather(
            temperature=float(current["temperature"]),
            weathercode=int(current["weathercode"]),
            time_iso=str(current["time"]),
        )


class Styles:
    def __init__(self, style: ttk.Style):
        self._style = style

    def apply(self) -> None:
        try:
            self._style.theme_use("clam")
        except Exception:
            pass

        self._style.configure("Card.TFrame", background=CARD_BG, padding=14)
        self._style.configure(
            "City.TLabel",
            background=CARD_BG,
            foreground="#e7eefc",
            font=("Segoe UI", 12, "bold"),
        )
        self._style.configure(
            "Weather.TLabel",
            background=CARD_BG,
            foreground="#c6d1ea",
            font=("Segoe UI", 16, "bold"),
        )
        self._style.configure(
            "Temp.TLabel",
            background=CARD_BG,
            foreground="#e7eefc",
            font=("Segoe UI", 18, "bold"),
        )
        self._style.configure(
            "Time.TLabel",
            background=CARD_BG,
            foreground="#9fb0d0",
            font=("Segoe UI", 9),
        )


class CityCard(ttk.Frame):
    def __init__(self, master: tk.Misc, city_name: str):
        super().__init__(master, style="Card.TFrame")

        self._lbl_city = ttk.Label(self, text=city_name, style="City.TLabel")
        self._lbl_city.grid(row=0, column=0, sticky="w")

        self._lbl_weather = ttk.Label(self, text="--", style="Weather.TLabel")
        self._lbl_weather.grid(row=1, column=0, sticky="w", pady=(6, 0))

        self._lbl_temp = ttk.Label(self, text="--.- °C", style="Temp.TLabel")
        self._lbl_temp.grid(row=0, column=1, rowspan=2, sticky="e")

        self._lbl_time = ttk.Label(self, text="--:--", style="Time.TLabel")
        self._lbl_time.grid(row=2, column=0, columnspan=2, sticky="w", pady=(6, 0))

        self.grid_columnconfigure(0, weight=1)

    def set_loading(self) -> None:
        self._lbl_weather.config(text="Loading")
        self._lbl_temp.config(text="...")
        self._lbl_time.config(text="")

    def set_error(self) -> None:
        self._lbl_weather.config(text="Error")
        self._lbl_temp.config(text="--")
        self._lbl_time.config(text="")

    def set_weather(self, cw: CurrentWeather) -> None:
        self._lbl_weather.config(text=weather_text(cw.weathercode))
        self._lbl_temp.config(text=f"{cw.temperature:.1f} °C")
        self._lbl_time.config(text=f"Observed {cw.observed_hhmm}")


class WeatherApp(tk.Tk):
    def __init__(self):
        super().__init__()

        self.title(APP_TITLE)
        self.geometry("640x720")
        self.configure(bg=APP_BG)

        Styles(ttk.Style(self)).apply()

        self._build_header()
        self._build_body()

        # ★STEP5追加
        self._cities = [City(name, lat, lon) for name, lat, lon in CITIES]
        self._client = OpenMeteoClient(timeout_sec=10)

        self._tick_clock()
        # ★STEP5追加
        self._refresh_sync()

    def _build_header(self) -> None:
        header = tk.Frame(self, bg=APP_BG)
        header.pack(fill="x", padx=16, pady=(16, 8))

        tk.Label(
            header,
            text=APP_TITLE,
            fg="#e7eefc",
            bg=APP_BG,
            font=("Segoe UI", 18, "bold"),
        ).pack(side="left")

        self._lbl_now = tk.Label(
            header,
            text="",
            fg="#9fb0d0",
            bg=APP_BG,
            font=("Segoe UI", 10),
        )
        self._lbl_now.pack(side="right")

    def _build_body(self) -> None:
        body = tk.Frame(self, bg=APP_BG)
        body.pack(fill="both", expand=True, padx=16, pady=8)

        self._cards: dict[str, CityCard] = {}
        for name, _, _ in CITIES:
            card = CityCard(body, name)
            card.pack(fill="x", pady=8)
            self._cards[name] = card

    def _tick_clock(self) -> None:
        now_str = datetime.now().strftime("%H:%M:%S")
        self._lbl_now.config(text=f"Now {now_str}")
        self.after(500, self._tick_clock)

    # ★STEP5追加
    def _refresh_sync(self) -> None:
        for card in self._cards.values():
            card.set_loading()

        for city in self._cities:
            card = self._cards.get(city.name)
            if card is None:
                continue

            try:
                cw = self._client.fetch_current(city)
                card.set_weather(cw)
            except Exception:
                card.set_error()


def main() -> None:
    app = WeatherApp()
    app.mainloop()


if __name__ == "__main__":
    main()

手順①:標準ライブラリでHTTP通信とJSON解析をする

import urllib.parse
import urllib.request
import json

Open-MeteoはWebAPIなので、データを取りに行くにはHTTP通信が必要です。以下をインポートします。

  • urllib.request
    • HTTP通信のため、WebAPIにアクセスしてデータを取る
  • urllib.parse
    • URLの組み立てのため
  • json
    • JSONをPythonの辞書に変換する

手順②:WebAPIで都市ごとの現在天気を取得する

class OpenMeteoClient:
    def fetch_current(self, city: City) -> CurrentWeather:
        ...

OpenMeteoClientは「APIを取得するクラス」です。
専用クラスにすることで、画面側と通信側が混ざらず、コードが読みやすくなります。

  • base_url
    • APIのURLの土台を作る
  • params
    • 緯度・経度・現在の天気・タイムゾーンを用意
  • url
    • 緯度・経度を組み合わせたURLを作成
  • with urllib.request.urlopen
    • APIにアクセスしてレスポンスを受け取る

そして WeatherAppクラスでは、

  • 都市リストを Cityの形に整える
  • OpenMeteoClientを用意する
  • _refresh_sync()で都市ごとに取得して表示する

という流れを実行しています。

これでAPIで取得した天気データを表示できるようになりました。
しかしこの時点では「同期で取得」なので、通信に時間がかかると一瞬画面が止まることがあります。

STEP6:非同期処理にする

この STEP では、天気の取得処理を別スレッドで動かし、非同期処理にすることでGUI 画面が固まらないようにします。
さらに「WeatherApp」に都市リストや API クライアントを外から渡す形にコードを整えます。

import json
# ★STEP6追加
import threading

import urllib.parse
import urllib.request
from dataclasses import dataclass
from datetime import datetime
# ★STEP6追加
from typing import Callable, Optional

import tkinter as tk
from tkinter import ttk


APP_TITLE = "World Weather Now"
APP_BG = "#0a1020"
CARD_BG = "#111b2e"

CITIES = [
    ("Tokyo", 35.6762, 139.6503),
    ("Osaka", 34.6937, 135.5023),
    ("Singapore", 1.3521, 103.8198),
    ("London", 51.5074, -0.1278),
    ("Paris", 48.8566, 2.3522),
    ("Berlin", 52.5200, 13.4050),
    ("New York", 40.7128, -74.0060),
    ("Los Angeles", 34.0522, -118.2437),
    ("Dubai", 25.2048, 55.2708),
]


def weather_text(code: int) -> str:
    if code == 0:
        return "Sunny"
    if code in (1, 2):
        return "Mostly Sunny"
    if code == 3:
        return "Cloudy"
    if code in (45, 48):
        return "Fog"
    if code in (51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 80, 81, 82):
        return "Rain"
    if code in (71, 73, 75, 77, 85, 86):
        return "Snow"
    if code in (95, 96, 99):
        return "Thunder"
    return "Unknown"


@dataclass(frozen=True)
class City:
    name: str
    lat: float
    lon: float


@dataclass(frozen=True)
class CurrentWeather:
    temperature: float
    weathercode: int
    time_iso: str

    @property
    def observed_hhmm(self) -> str:
        try:
            return datetime.fromisoformat(self.time_iso).strftime("%H:%M")
        except Exception:
            return "--:--"


class OpenMeteoClient:
    def __init__(self, timeout_sec: int = 10):
        self.timeout_sec = timeout_sec

    def fetch_current(self, city: City) -> CurrentWeather:
        base_url = "https://api.open-meteo.com/v1/forecast"
        params = {
            "latitude": f"{city.lat:.4f}",
            "longitude": f"{city.lon:.4f}",
            "current_weather": "true",
            "timezone": "auto",
        }
        url = base_url + "?" + urllib.parse.urlencode(params)

        with urllib.request.urlopen(url, timeout=self.timeout_sec) as resp:
            data = json.loads(resp.read().decode("utf-8"))

        current = data["current_weather"]
        return CurrentWeather(
            temperature=float(current["temperature"]),
            weathercode=int(current["weathercode"]),
            time_iso=str(current["time"]),
        )


class Styles:
    def __init__(self, style: ttk.Style):
        self._style = style

    def apply(self) -> None:
        try:
            self._style.theme_use("clam")
        except Exception:
            pass

        self._style.configure("Card.TFrame", background=CARD_BG, padding=14)
        self._style.configure(
            "City.TLabel",
            background=CARD_BG,
            foreground="#e7eefc",
            font=("Segoe UI", 12, "bold"),
        )
        self._style.configure(
            "Weather.TLabel",
            background=CARD_BG,
            foreground="#c6d1ea",
            font=("Segoe UI", 16, "bold"),
        )
        self._style.configure(
            "Temp.TLabel",
            background=CARD_BG,
            foreground="#e7eefc",
            font=("Segoe UI", 18, "bold"),
        )
        self._style.configure(
            "Time.TLabel",
            background=CARD_BG,
            foreground="#9fb0d0",
            font=("Segoe UI", 9),
        )


class CityCard(ttk.Frame):
    def __init__(self, master: tk.Misc, city_name: str):
        super().__init__(master, style="Card.TFrame")
        self._city_name = city_name  # ★STEP6追加

        self._lbl_city = ttk.Label(self, text=city_name, style="City.TLabel")
        self._lbl_city.grid(row=0, column=0, sticky="w")

        self._lbl_weather = ttk.Label(self, text="--", style="Weather.TLabel")
        self._lbl_weather.grid(row=1, column=0, sticky="w", pady=(6, 0))

        self._lbl_temp = ttk.Label(self, text="--.- °C", style="Temp.TLabel")
        self._lbl_temp.grid(row=0, column=1, rowspan=2, sticky="e")

        self._lbl_time = ttk.Label(self, text="--:--", style="Time.TLabel")
        self._lbl_time.grid(row=2, column=0, columnspan=2, sticky="w", pady=(6, 0))

        self.grid_columnconfigure(0, weight=1)

    def set_loading(self) -> None:
        self._lbl_weather.config(text="Loading")
        self._lbl_temp.config(text="...")
        self._lbl_time.config(text="")

    def set_error(self) -> None:
        self._lbl_weather.config(text="Error")
        self._lbl_temp.config(text="--")
        self._lbl_time.config(text="")

    def set_weather(self, cw: CurrentWeather) -> None:
        self._lbl_weather.config(text=weather_text(cw.weathercode))
        self._lbl_temp.config(text=f"{cw.temperature:.1f} °C")
        self._lbl_time.config(text=f"Observed {cw.observed_hhmm}")


class WeatherApp(tk.Tk):
    # ★STEP6変更
    def __init__(
        self,
        cities: list[City],
        client: OpenMeteoClient,
        now_provider: Callable[[], datetime] = datetime.now,
    ):
        # ★STEP6変更
        super().__init__()
        self._cities = cities  # ★STEP6変更(下段空から移動して変更)
        self._client = client  # ★STEP6変更(下段空から移動して変更)
        self._now_provider = now_provider  # ★STEP6追加

        self.title(APP_TITLE)
        self.geometry("640x720")
        self.configure(bg=APP_BG)

        Styles(ttk.Style(self)).apply()

        self._build_header()
        self._build_body()

        self._tick_clock()
        self._refresh_async()  # ★STEP6変更

    def _build_header(self) -> None:
        header = tk.Frame(self, bg=APP_BG)
        header.pack(fill="x", padx=16, pady=(16, 8))

        tk.Label(
            header,
            text=APP_TITLE,
            fg="#e7eefc",
            bg=APP_BG,
            font=("Segoe UI", 18, "bold"),
        ).pack(side="left")

        self._lbl_now = tk.Label(
            header,
            text="",
            fg="#9fb0d0",
            bg=APP_BG,
            font=("Segoe UI", 10),
        )
        self._lbl_now.pack(side="right")

    def _build_body(self) -> None:
        body = tk.Frame(self, bg=APP_BG)
        body.pack(fill="both", expand=True, padx=16, pady=8)

        self._cards: dict[str, CityCard] = {}
        # ★STEP6変更
        for city in self._cities:
            card = CityCard(body, city.name)
            card.pack(fill="x", pady=8)
            self._cards[city.name] = card

    # ★STEP6変更
    def _tick_clock(self) -> None:
        self._lbl_now.config(text=f"Now {self._now_provider().strftime('%H:%M:%S')}")
        self.after(500, self._tick_clock)

    # ★STEP6変更
    def _refresh_async(self) -> None:
        for card in self._cards.values():
            card.set_loading()

        threading.Thread(target=self._worker_fetch_all, daemon=True).start()

    # ★STEP6追加
    def _worker_fetch_all(self) -> None:
        for city in self._cities:
            card = self._cards.get(city.name)
            if card is None:
                continue

            try:
                cw = self._client.fetch_current(city)
                self.after(0, card.set_weather, cw)
            except Exception:
                self.after(0, card.set_error)


# ★STEP6変更
def main() -> None:
    cities = [City(name, lat, lon) for name, lat, lon in CITIES]
    app = WeatherApp(cities=cities, client=OpenMeteoClient(timeout_sec=10))
    app.mainloop()


if __name__ == "__main__":
    main()

手順①:「threading」と「Callable」をインポートする

import threading
from typing import Callable, Optional

STEP5のように通信をそのまま実行すると、通信の間、画面更新が止まり、ウィンドウが固まったように見えることがあります。
そのため、以下をインポートします。

  • threading
    • 通信部分を「別スレッド」で動かせるようにする
  • Callable
    • 関数を引数として受け取るために必要

手順②:WeatherAppの「init」を変更する

class WeatherApp(tk.Tk):
  def __init__(
      self,
      cities: list[City],
      client: OpenMeteoClient,
      now_provider: Callable[[], datetime] = datetime.now,
  ):
  
  ...

      self._cities = cities
      self._client = client
      self._now_provider = now_provider

STEP5ではWeatherAppの中で都市一覧やクライアントを作っていましたが、STEP6では外から渡す形に変えています。

また、受け取った cities / client / now_provider をインスタンスに保持します。

  • self._cities
    • このアプリが扱う都市一覧を保存
  • self._client
    • 天気取得のためのクライアントを保存
  • self._now_provider
    • 時刻を作る方法を保存

手順③:天気取得を「_refresh_async」に変更して画面を止めないようにする

self._refresh_async()

通信が遅い可能性があるので、画面更新とは別に動かします。
「_refresh_async()」では、まず全カードをLoadingにしてから、別スレッドを起動します。

threading.Thread(target=self._worker_fetch_all, daemon=True).start()

ここで別スレッドを開始して、通信処理はそちらで動かします。

手順④:別スレッドで天気を取得し画面を更新する

def _worker_fetch_all(self) -> None:
    ...
    cw = self._client.fetch_current(city)
    self.after(0, card.set_weather, cw)
  • fetch_current(city)
    • API通信し都市の現在天気を取得
  • after(0, …)
    • 画面更新はメインスレッドで実施
  • card.set_weather(cw)
    • カードの表示を天気データで更新

手順⑤:mainでcitiesを作ってWeatherAppに渡す

cities = [City(name, lat, lon) for name, lat, lon in CITIES]
app = WeatherApp(cities=cities, client=OpenMeteoClient(timeout_sec=10))
  • cities =…
    • CITIESを Cityオブジェクトのリストに変換
  • app =…
    • WeatherApp を cities と client を渡して起動

WeatherApp が都市一覧を外から受け取る形になったので、main() 側で City オブジェクトを作って渡します。

これで WeatherApp の責任が「画面を作ること」に寄り、コードが整理されます。

これでコードとしては完成です。
最期に記事冒頭に「完成イメージ」で紹介したコメントを参考にコメントを整えてください。

完成

以上でPythonで作る「お天気アプリ」の完成です。

ぜひ、コードをコピペするのではなく、実際にコードを打って作ってみてください。

お疲れさまでした。

ABOUT ME
タカヒデ
タカヒデ
ITを楽しく勉強中
通信会社の企画職で働く30代 非エンジニアでありながら半分趣味でITエンジニアリングやプログラミングを学習中 IT初心者が楽しいと思えるように発信することを目指しています ↓Xもやっているのでフォローしてね
記事URLをコピーしました