
Posted on · Updated on · 3 min read
How to Call a REST API in Python: A Practical Guide With Free APIs
On this page · 12 sections
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 httpxrequests 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 Nonehttpbin 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") # 194Using 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 |
|---|---|---|
Weather forecasts | No | |
Exchange rates | No | |
Crypto prices | No (demo key for more) | |
Pokémon data | No | |
Book search | No | |
Quiz questions | No | |
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.