Chapter 02 · Section II · 26 min read
Calling APIs for data (weather, NEPSE, exchange rates)
APIs are how the modern internet hands data to programs. Five lines of Python get you live weather, exchange rates, NEPSE quotes, and a thousand other feeds. Learn the shape once and the rest are variations.
An API — Application Programming Interface — is the way one program asks another for data. Where a human visits a website, a program calls an API and gets the same data in a machine-readable format (almost always JSON). Once you can call one API, you can call most of them — the shape barely changes. This section walks through the canonical pattern with three concrete Nepali examples: today’s Kathmandu weather, the NPR-USD exchange rate, and a NEPSE index value.
The basic shape of an API call
In Python, the standard library for API calls is requests. Install once per project:
pip install requests
The minimal call:
import requests
response = requests.get("https://api.example.com/some-endpoint")
data = response.json()
print(data)
Three lines do almost everything:
requests.get(url)makes an HTTP GET request and returns aResponseobject.response.json()parses the JSON body into a Python dictionary or list.print(data)to inspect.
That is the entire vocabulary. Everything else is variations on what URL you call, what headers you send, and what authentication you need.
A first real call: weather
Open-Meteo is a free, no-key weather API. To get current conditions for Kathmandu:
import requests
response = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": 27.7172,
"longitude": 85.3240,
"current_weather": True,
},
)
data = response.json()
print(data["current_weather"])
The params= argument turns into a query string in the URL. The full URL Python actually called is https://api.open-meteo.com/v1/forecast?latitude=27.7172&longitude=85.3240¤t_weather=true. Pandas, the requests library, and most HTTP tools handle URL-encoding for you — you just provide a Python dict.
A typical response:
{
'temperature': 24.3,
'windspeed': 7.6,
'winddirection': 180,
'weathercode': 3,
'time': '2026-06-27T13:00'
}
Five real lines, one piece of useful data.
A second call: exchange rates
Exchange rate APIs are a great example because nearly every Nepali financial project touches them. A common free option is the exchangerate-api.com open endpoint:
response = requests.get("https://open.er-api.com/v6/latest/USD")
data = response.json()
npr_per_usd = data["rates"]["NPR"]
print(f"1 USD = {npr_per_usd} NPR")
Output:
1 USD = 134.15 NPR
For some APIs you do not need a key. For others you do. Anytime you do, refer back to the secrets discipline from Course 01, Chapter 4: API keys live in .env, are loaded with python-dotenv, and never appear in committed code.
A third call: NEPSE-style endpoints
NEPSE-related APIs vary in quality and availability — some are official, some are scraped by third parties, some come and go. The shape is identical to the other two:
response = requests.get(
"https://api.example-nepse-feed.com/v1/index",
params={"date": "2026-06-27"},
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
if response.status_code == 200:
data = response.json()
print(data)
else:
print(f"Request failed with status {response.status_code}")
Two new things in this version:
headers=for authentication. Most paid APIs require a Bearer token or API key passed in a header. Read the API’s documentation to see which.response.status_code— a code that tells you whether the request succeeded.200is success.401is “you are not authenticated”.429is “you are calling too fast — slow down”.500is “the server broke”. Therequestslibrary does not raise an error for these by default; you check.
A complete, slightly-more-real example
The shape of nearly every API-call script you will write:
import os
import requests
from dotenv import load_dotenv
load_dotenv()
def fetch_weather(city_lat: float, city_lon: float) -> dict:
response = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": city_lat,
"longitude": city_lon,
"current_weather": True,
},
timeout=10,
)
response.raise_for_status()
return response.json()["current_weather"]
if __name__ == "__main__":
weather = fetch_weather(27.7172, 85.3240)
print(f"Kathmandu now: {weather['temperature']}°C, wind {weather['windspeed']} km/h")
Three small details worth carrying with you:
timeout=10— never call an API without a timeout. If the server hangs, your script hangs forever. Ten seconds is plenty for most APIs.response.raise_for_status()— turns non-200 responses into a Python exception you can catch. Easier than writingif response.status_code != 200everywhere.- One function per endpoint. Wrap the call in a function with clear inputs and outputs. Makes the rest of the program testable, and lets you stub the API the way you stubbed NEPSE in Course 01.
Rate limits — the speed bump every builder hits
Every public API has limits on how often you can call it. Free tiers often allow 60 requests per minute, or 1,000 per day. Paid tiers go higher. Hit the limit and you get a 429 Too Many Requests response.
The polite ways to handle this:
- Cache responses. If you call the weather API every five minutes for one city, save the response locally for five minutes and skip the call if it is still fresh.
- Sleep between calls. When looping over a list of items, add
time.sleep(0.5)between calls. - Exponential backoff. If you get a
429, wait 1 second and retry. If you get it again, wait 2 seconds. Then 4, 8, 16. Eventually you fall under the rate limit or you stop.
A small wrapper that does the third one:
import time
def get_with_backoff(url: str, max_retries: int = 5, **kwargs) -> dict:
delay = 1.0
for _ in range(max_retries):
response = requests.get(url, timeout=10, **kwargs)
if response.status_code != 429:
response.raise_for_status()
return response.json()
time.sleep(delay)
delay *= 2
raise RuntimeError("Rate limited even after retries")
You write this once per project and reuse it for every API call. Saves real time.
When the API returns wrong-looking data
APIs are not infallible. The shape of the response can change overnight. The data can be stale. The endpoint can return 200 OK with an empty body. A useful defensive pattern:
data = response.json()
if not data:
print("API returned empty body")
return None
if "current_weather" not in data:
print(f"Unexpected response shape: {list(data.keys())}")
return None
return data["current_weather"]
You will be tempted to write nothing of this kind and trust the API. Within a year of building, you will be glad you wrote it.
Check your understanding
Quick check
—A script that fetches today's NEPSE close every five minutes starts returning `429 Too Many Requests` after running for an hour. What is the most useful first response, from this section's vocabulary?
Quick check
—A teammate writes: response = requests.get('https://api.example.com/data') data = response.json() process(data) What is the most important defensive change to make before shipping this?
What comes next
Files and APIs cover most of what you can fetch legally and cleanly. Sometimes the data you want is on a web page with no API — a Nepali news site, a list of school addresses on a ministry page, a public NEPSE table without a feed. Scraping — extracting data from rendered web pages — is the last fetching technique we cover. It comes with constraints worth understanding, both technical and ethical.