To call a REST API in Python, install requests , send a request with requests.get(url, params=..., timeout=10), call raise_for_status() to catch errors, and read the JSON with .json(). That's three lines. The rest of this guide covers what you need for real projects: API keys, POST requests, error handling, retries, pagination and async calls.
Every example below calls a free public API that needed no key in our checks in October 2026, so you can paste and run them. For a ranked list of APIs that work well from Python, see free APIs for Python.
Setup
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install requests httpx
requests is the standard synchronous HTTP library. httpx has an almost identical API and adds async support; we'll use it at the end.
1. Your first GET request
import requests
res = requests.get("https://catfact.ninja/fact", timeout=10)
res.raise_for_status() # raise an error for 4xx/5xx responses
data = res.json() # parse the JSON body into a dict
print(data["fact"])
Three habits to build from day one:
-
Always pass
timeout. Without it, a hung server can block your program forever. -
Always call
raise_for_status(). Otherwise a404or500silently gives you an error page instead of data. -
Use
.json(), notjson.loads(res.text). It handles encoding for you.
2. Query parameters
Don't build query strings by hand. Pass a dict to params and requests handles the encoding:
import requests
params = {
"latitude": 51.51,
"longitude": -0.13,
"current": "temperature_2m,wind_speed_10m",
"timezone": "auto",
}
res = requests.get("https://api.open-meteo.com/v1/forecast", params=params, timeout=10)
res.raise_for_status()
current = res.json()["current"]
print(f"{current['temperature_2m']}°C, wind {current['wind_speed_10m']} km/h")
This is the Open-Meteo weather API: free, keyless and global.
3. Sending an API key
Most APIs with a free tier give you a key after sign-up. Never hard-code it. Read it from an environment variable and send it the way the docs say: usually a header, sometimes a query parameter.
import os
import requests
API_KEY = os.environ["WEATHERAPI_KEY"] # export WEATHERAPI_KEY=... in your shell or .env
# Key as a query parameter (WeatherAPI.com style)
res = requests.get(
"https://api.weatherapi.com/v1/current.json",
params={"key": API_KEY, "q": "London"},
timeout=10,
)
# Key as a header (the most common pattern)
res = requests.get(
"https://api.example.com/v1/items",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10,
)
For local development, python-dotenv loads a .env file into os.environ. Add .env to .gitignore so the key never reaches GitHub.
4. POST requests with a JSON body
Use the json= argument. It serialises the dict and sets the Content-Type: application/json header for you.
import requests
new_post = {"title": "Hello from Python", "body": "Testing a POST request", "userId": 1}
res = requests.post("https://jsonplaceholder.typicode.com/posts", json=new_post, timeout=10)
res.raise_for_status()
print(res.status_code, res.json()) # 201 {'title': ..., 'id': 101}
JSONPlaceholder fakes writes, so it's safe to experiment with. For more test endpoints, see our guide to free APIs for testing.
5. Handling errors properly
Network calls fail. Distinguish the three kinds of failure so you can handle each one:
import requests
def get_json(url: str, **kwargs):
try:
res = requests.get(url, timeout=10, **kwargs)
res.raise_for_status()
return res.json()
except requests.Timeout:
print("The API took too long to answer")
except requests.HTTPError as e:
status = e.response.status_code
if status == 429:
print("Rate limited: slow down or wait for the reset")
elif status in (401, 403):
print("Check your API key")
else:
print(f"HTTP error {status}")
except requests.RequestException as e:
print(f"Network problem: {e}")
return None
print(get_json("https://httpbin.org/status/503")) # HTTP error 503, then None
httpbin can return any status code (/status/{code}) or delay its answer (/delay/{seconds}), which is handy for testing this code.
6. Retries with backoff
Free APIs return occasional 429 and 5xx errors. Rather than writing your own retry loop, mount urllib3's Retry on a Session:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retry = Retry(
total=3,
backoff_factor=0.5, # waits 0.5 s, 1 s, 2 s between attempts
status_forcelist=(429, 500, 502, 503, 504),
allowed_methods=("GET",),
respect_retry_after_header=True,
)
session.mount("https://", HTTPAdapter(max_retries=retry))
session.headers.update({"User-Agent": "my-app/1.0 (you@example.com)"})
res = session.get("https://api.coingecko.com/api/v3/simple/price",
params={"ids": "bitcoin,ethereum", "vs_currencies": "usd"}, timeout=10)
print(res.json())
A Session also reuses connections, which makes repeated calls to the same host noticeably faster. Setting a descriptive User-Agent helps too: some APIs reject the default one. ipapi.co and OpenStreetMap's Nominatim both refused generic clients in our tests.
7. Pagination
APIs return large collections in pages. The two common styles are page numbers and offset/limit. Here's offset/limit with DummyJSON:
import requests
def all_products():
skip, limit = 0, 50
while True:
res = requests.get("https://dummyjson.com/products",
params={"limit": limit, "skip": skip, "select": "title,price"}, timeout=10)
res.raise_for_status()
page = res.json()
yield from page["products"]
skip += limit
if skip >= page["total"]:
break
products = list(all_products())
print(len(products), "products") # 194
Using a generator (yield from) means the caller can stop early without fetching every page.
8. Many requests at once with async httpx
When you need dozens of independent calls, running them concurrently is much faster than a loop. httpx.AsyncClient with asyncio.gather does that:
import asyncio
import httpx
POKEMON = ["pikachu", "bulbasaur", "charmander", "squirtle", "eevee"]
async def fetch(client: httpx.AsyncClient, name: str):
res = await client.get(f"https://pokeapi.co/api/v2/pokemon/{name}")
res.raise_for_status()
data = res.json()
return name, data["height"], data["weight"]
async def main():
async with httpx.AsyncClient(timeout=10) as client:
results = await asyncio.gather(*(fetch(client, n) for n in POKEMON))
for name, height, weight in results:
print(f"{name}: {height} dm, {weight} hg")
asyncio.run(main())
Be polite: concurrency multiplies your request rate. For large batches, cap it with an asyncio.Semaphore so you stay within the API's rate limit.
9. Turning responses into typed objects
For bigger projects, convert raw dicts into typed objects so your editor can autocomplete fields and catch typos:
from dataclasses import dataclass
import requests
@dataclass
class Rate:
base: str
date: str
rates: dict[str, float]
res = requests.get("https://api.frankfurter.dev/v1/latest",
params={"base": "USD", "symbols": "EUR,GBP,JPY"}, timeout=10)
res.raise_for_status()
rate = Rate(**{k: res.json()[k] for k in ("base", "date", "rates")})
print(rate.rates["EUR"])
Frankfurter serves central-bank exchange rates with no key. Pydantic is the next step up if you want validation as well as types.
Free APIs to practise with
| API | Try | Key |
|---|---|---|
| Open-Meteo | Weather forecasts | No |
| Frankfurter | Exchange rates | No |
| CoinGecko | Crypto prices | No (demo key for more) |
| PokéAPI | Pokémon data | No |
| Open Library | Book search | No |
| Open Trivia | Quiz questions | No |
| Spaceflight News | Space news | No |
Our Python API guide also lists endpoints our daily health check confirmed are returning JSON, with step-by-step guides for each.
FAQ
What is the best Python library for calling APIs?
requests for synchronous code: it's simple and universal. httpx if you need async or HTTP/2; its API is nearly identical.
How do I parse a JSON response in Python?
Call response.json(). It returns a dict or list. Call response.raise_for_status() first so you don't try to parse an error page.
How do I keep my API key secret in Python?
Store it in an environment variable (or a .env file loaded with python-dotenv), read it with os.environ, and add .env to .gitignore.
Why does my request hang?
You probably didn't set a timeout. Always pass timeout=10 (or similar) to every request.

Top comments (0)