Calling APIs in Python: requests, JSON & Authentication
requests is how Python asks.After this lesson, you will be able to:
- Install and use the requests library to make HTTP GET and POST requests
- Parse JSON responses from APIs into Python dicts and lists
- Send authentication headers (API keys) with every request
- Handle errors gracefully when APIs fail or rate-limit you
- Call a real API directly with raw HTTP — no special library required
Before You Start
#What is an API?
#Installing requests
pip install requests
requests is the most downloaded Python package in the world, used in over 1 million projects.#Your First API Call
import requests
# GET request — fetch data from a URL
response = requests.get("https://api.github.com/users/python")
# Check if it worked
print(response.status_code) # 200 = success, 404 = not found, 429 = rate limited
# Get the JSON data as a Python dict
data = response.json()
print(data["name"]) # "Python"
print(data["public_repos"]) # number of public repositoriesThe two steps every API call follows
- Send a request → get a
responseobject - Parse the response with
.json()→ get a Python dict
#Anatomy of an HTTP Response
response = requests.get("https://api.github.com/users/python")
# Status code tells you what happened
print(response.status_code) # 200 = OK, 201 = Created, 400 = Bad Request
print(response.ok) # True if status_code < 400
# Headers contain metadata
print(response.headers["Content-Type"]) # "application/json; charset=utf-8"
# The body — as text or parsed JSON
print(response.text) # raw JSON string
print(response.json()) # Python dict (use this)Status codes you'll see every day
200 OK— worked perfectly201 Created— created a new resource (POST succeeded)400 Bad Request— you sent something wrong401 Unauthorized— you need an API key429 Too Many Requests— you hit the rate limit, slow down500 Internal Server Error— the server broke, not your fault
#Sending Authentication: API Keys
Most real APIs require an API key — your "membership card" that proves you're allowed in.
import requests
import os
API_KEY = os.environ.get("MY_API_KEY") # Always read from environment, never hardcode
# Method 1: Authorization header (most common)
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
response = requests.get("https://api.example.com/data", headers=headers)
# Method 2: Query parameter (some APIs use this)
response = requests.get(
"https://api.example.com/data",
params={"api_key": API_KEY, "format": "json"}
)
# Method 3: Custom header (some APIs use their own header names)
headers = {
"x-api-key": API_KEY,
"api-version": "2024-01-01",
"content-type": "application/json"
}You committed an API key, noticed within a minute, and pushed a follow-up commit removing it. Is the key safe?
#POST Requests: Sending Data
GET fetches data. POST sends data — you use it when the request itself carries information, like a form or a new record.
import requests
import json
# POST request — send JSON data to an API
url = "https://api.example.com/v1/orders"
headers = {
"x-api-key": "your-key-here",
"content-type": "application/json"
}
payload = {
"customer": "Asha",
"items": [
{"name": "Notebook", "quantity": 2},
{"name": "Pen", "quantity": 5}
]
}
response = requests.post(url, headers=headers, json=payload)
data = response.json()
print(data["order_id"], data["status"]) # ord_7741 confirmedjson=payload vs data=json.dumps(payload)
json=payload automatically sets the Content-Type header and serializes the dict. Use it.#Error Handling: APIs Fail All the Time
import requests
from requests.exceptions import RequestException, Timeout, ConnectionError
def call_api_safely(url: str, headers: dict) -> dict | None:
"""Call an API with proper error handling."""
try:
response = requests.get(url, headers=headers, timeout=10) # 10s timeout
# Raise an exception for bad status codes (4xx, 5xx)
response.raise_for_status()
return response.json()
except Timeout:
print("Request timed out — server took too long")
except ConnectionError:
print("No internet connection or server is down")
except requests.HTTPError as e:
print(f"HTTP error: {e.response.status_code} — {e.response.text}")
except RequestException as e:
print(f"Something went wrong: {e}")
return None#Handling Pagination
Many APIs return results in pages — they can't send 10,000 results at once.
import time
def get_all_results(base_url: str, headers: dict) -> list:
"""Fetch all pages of results from a paginated API."""
results = []
page = 1
while True:
response = requests.get(
base_url,
headers=headers,
params={"page": page, "per_page": 100},
timeout=10,
)
response.raise_for_status()
data = response.json()
# No more results — stop
if not data:
break
results.extend(data)
page += 1
# Respect rate limits — pause between requests
time.sleep(0.1)
return results#A Complete Real-World Example
import requests
import os
import json
def get_repo_info(username: str, repo: str) -> dict:
"""Fetch GitHub repository info — no auth needed for public repos."""
url = f"https://api.github.com/repos/{username}/{repo}"
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
return {
"name": data["full_name"],
"stars": data["stargazers_count"],
"language": data["language"],
"description": data["description"],
"last_updated": data["updated_at"][:10], # just the date part
}
# Usage
info = get_repo_info("python", "cpython")
print(f"⭐ {info['stars']} stars")
print(f"🔤 Written in {info['language']}")
print(f"📝 {info['description']}")Interactive Lab
See how an API call works step by step — request structure, response parsing, and error handling
Key Takeaways
- requests.get(url) fetches data; requests.post(url, json=payload) sends data — these two cover 90% of API usage
- response.json() converts the JSON response to a Python dict you can work with immediately
- Always send API keys in headers (Authorization: Bearer KEY), never in the URL or code
- Always set timeout=N seconds and use response.raise_for_status() to catch errors before they become silent bugs
What does response.raise_for_status() do?