Skip to contentAPILayer: production APIs with free plansExplore APILayer
Cover illustration: How to Call a REST API in Python: A Practical Guide With Free APIs
Favour Onuoha

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

Bash
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

Python
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 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:

Python
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.

Python
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.

Python
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:

Python
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:

Python
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:

Python
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:

Python
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:

Python
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.