DEV Community

public apis
public apis

Posted on Originally published at publicapis.lol on

How to Call a REST API in Python: A Practical Guide With Free APIs

Cover illustration: How to Call a REST API in Python: A Practical Guide With Free APIs

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
Enter fullscreen mode Exit fullscreen mode

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"])
Enter fullscreen mode Exit fullscreen mode

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 a 404 or 500 silently gives you an error page instead of data.
  • Use .json(), not json.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")
Enter fullscreen mode Exit fullscreen mode

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,
)
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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"])
Enter fullscreen mode Exit fullscreen mode

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)