What you'll learn
- What an API is and how web APIs work
- How to send HTTP requests with Python's requests library
- How to read JSON responses and handle errors
- How to use query parameters, headers, and timeouts
- How to make GET and POST requests
- Build a Mini Pokedex that fetches live data from a real API
1) What is an API?
- API stands for Application Programming Interface. It's a set of rules that lets one program talk to another.
- Think of it like a restaurant menu. You (the client) ask for a dish (data) using the menu's format (the API), and the kitchen (the server) returns it.
- Web APIs use HTTP (the same protocol your browser uses) so your Python code can request data from websites and services.
2) How the web talks (HTTP basics)
Request: what your program sends
- Method: GET (read), POST (create), PUT/PATCH (update), DELETE (remove)
- URL: where to send it
- Headers: extra info (like "I want JSON")
- Body: the data you send (mostly for POST/PUT)
Response: what the server sends back
- Status code: 200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests, 500 Server Error
- Headers: extra info (like rate limits)
- Body: the data (often JSON)
3) JSON (the data format you'll see most)
JSON looks like Python dictionaries/lists. Example:
{
"name": "pikachu",
"types": ["electric"],
"stats": {"hp": 35, "attack": 55}
}In Python, you'll turn JSON into dict/list with response.json().
4) Meet the requests library
Install it:
- In Terminal:
pip install requests - In a Jupyter notebook:
%pip install requests
Basic GET request example (GitHub public API):
import requests
url = "https://api.github.com/repos/python/cpython"
resp = requests.get(url, timeout=10) # timeout prevents hanging forever
print("Status:", resp.status_code)
resp.raise_for_status() # raises an error for 4xx/5xx
data = resp.json() # parse JSON into a Python dict
print("Repo name:", data["name"])
print("Stars:", data["stargazers_count"])
print("License:", data["license"]["name"] if data["license"] else "No license")
# Some APIs include rate-limit info in headers:
print("Rate limit remaining:", resp.headers.get("X-RateLimit-Remaining"))5) Query parameters (asking for specific data)
Instead of building URLs manually, pass params as a dict. Example with a free weather API (no key needed):
import requests
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": 52.52,
"longitude": 13.41,
"hourly": "temperature_2m"
}
resp = requests.get(url, params=params, timeout=10)
resp.raise_for_status()
data = resp.json()
print("Timezone:", data["timezone"])
print("First 5 temps:", data["hourly"]["temperature_2m"][:5])
print("Final URL sent:", resp.url) # shows the URL with ?latitude=...&...6) Headers (asking politely)
Some APIs want specific headers like Accept or User-Agent.
import requests
url = "https://api.github.com/repos/python/cpython"
headers = {
"Accept": "application/vnd.github+json",
"User-Agent": "PyVerse-Student-Example" # be descriptive and polite
}
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
print(resp.json()["full_name"])7) Handling errors and timeouts
- Use
timeoutto avoid hanging forever. - Use
try/exceptandraise_for_statusto handle issues.
import requests
from requests.exceptions import HTTPError, Timeout, RequestException
url = "https://api.github.com/this/does/not/exist"
try:
resp = requests.get(url, timeout=5)
resp.raise_for_status()
data = resp.json()
except Timeout:
print("The request timed out. Try again later.")
except HTTPError as e:
print("HTTP error:", e, "Status:", getattr(e.response, "status_code", None))
except RequestException as e:
print("Network or other error:", e)
else:
print("Success:", data)Handling 429 Too Many Requests (basic backoff):
import time
import requests
url = "https://api.github.com/rate_limit"
for attempt in range(3):
r = requests.get(url, timeout=10)
if r.status_code == 429:
wait = (attempt + 1) * 2 # 2s, 4s, 6s
print(f"Rate limited. Waiting {wait}s...")
time.sleep(wait)
continue
r.raise_for_status()
print(r.json())
break8) Making a POST request (creating data)
Use JSONPlaceholder (a free test API) to simulate creating a post:
import requests
url = "https://jsonplaceholder.typicode.com/posts"
payload = {
"title": "My first API post",
"body": "Hello from Python!",
"userId": 42
}
resp = requests.post(url, json=payload, timeout=10) # json= auto-encodes and sets header
resp.raise_for_status()
print("Status:", resp.status_code) # often 201 Created
print("Returned JSON:", resp.json()) # includes an id9) Authentication (API keys and tokens)
- Some APIs require keys. Never hard-code secrets in your script.
- Store your key in an environment variable, then read it in Python:
import os
import requests
API_KEY = os.getenv("MY_API_KEY") # set this in your system first
url = "https://example.com/data"
headers = {"Authorization": f"Bearer {API_KEY}"}
# or use params = {"apikey": API_KEY} if the API uses query params
# resp = requests.get(url, headers=headers, timeout=10)Check the API docs to see if they want keys in headers or params.
Practical Project: Mini Pokedex (CLI)
Goal
Build a command-line Pokedex using the free PokeAPI. You'll search for a Pokémon by name and display its info.
What you'll use
- requests for HTTP calls
- JSON parsing
- Error handling and timeouts
- Optional: download the sprite image
API docs: https://pokeapi.co/
Endpoint we'll use: https://pokeapi.co/api/v2/pokemon/{name}
Full program
# mini_pokedex.py
import requests
import random
from requests.exceptions import HTTPError, Timeout
API_BASE = "https://pokeapi.co/api/v2/pokemon/"
def get_pokemon(name_or_id):
"""
Fetch Pokémon data by name or id from PokeAPI.
Returns a dict with key info, or None if not found.
"""
url = API_BASE + str(name_or_id).strip().lower()
try:
resp = requests.get(url, timeout=10)
resp.raise_for_status()
except Timeout:
print("Request timed out. Check your internet and try again.")
return None
except HTTPError as e:
if e.response is not None and e.response.status_code == 404:
print("Pokémon not found. Check the name or id.")
else:
print(f"HTTP error: {e}")
return None
except Exception as e:
print(f"Network error: {e}")
return None
raw = resp.json()
# Extract clean info
name = raw["name"].title()
pid = raw["id"]
types = [t["type"]["name"].title() for t in raw["types"]]
stats_map = {s["stat"]["name"]: s["base_stat"] for s in raw["stats"]}
info = {
"id": pid,
"name": name,
"height": raw["height"], # decimeters
"weight": raw["weight"], # hectograms
"types": types,
"stats": {
"hp": stats_map.get("hp"),
"attack": stats_map.get("attack"),
"defense": stats_map.get("defense"),
"speed": stats_map.get("speed"),
},
"sprite": raw["sprites"]["front_default"],
}
return info
def print_pokemon(p):
"""Pretty-print selected Pokémon info."""
if not p:
return
print("-" * 40)
print(f"{p['name']} (ID: {p['id']})")
print(f"Types: {', '.join(p['types'])}")
print(f"Height: {p['height']} dm Weight: {p['weight']} hg")
s = p["stats"]
print(f"Stats -> HP: {s['hp']} ATK: {s['attack']} DEF: {s['defense']} SPD: {s['speed']}")
print(f"Sprite URL: {p['sprite']}")
print("-" * 40)
def download_sprite(url, filename):
"""Download the sprite image if a URL exists."""
if not url:
print("No sprite available.")
return
try:
r = requests.get(url, timeout=10)
r.raise_for_status()
with open(filename, "wb") as f:
f.write(r.content)
print(f"Saved sprite to {filename}")
except Exception as e:
print(f"Could not download sprite: {e}")
def main():
print("Mini Pokedex")
print("Type a Pokémon name or id (e.g., pikachu or 25).")
print("Type 'random' for a random Pokémon, or 'quit' to exit.")
while True:
q = input("Search: ").strip()
if q.lower() in {"quit", "exit"}:
break
if q.lower() == "random":
# PokeAPI has many; choose a safe visible range
q = random.randint(1, 898)
p = get_pokemon(q)
print_pokemon(p)
if p and p["sprite"]:
choice = input("Download sprite image? (y/n): ").strip().lower()
if choice == "y":
filename = f"{p['name'].lower()}_{p['id']}.png"
download_sprite(p["sprite"], filename)
print("Goodbye!")
if __name__ == "__main__":
main()How to run
- Save as
mini_pokedex.py - Install requests:
pip install requests - Run:
python mini_pokedex.py - Try searches like pikachu, charizard, bulbasaur, or random
What you learned in this project
- Building a URL endpoint and sending GET requests
- Handling status codes and timeouts
- Reading JSON and extracting nested fields
- Optional file download using requests
Optional extensions
- Cache results in a local JSON file so repeated searches don't call the API again.
- Add more stats (special-attack, special-defense).
- Support listing abilities and moves.
- Add a command to compare two Pokémon by speed, attack, etc.
Pro tips and best practices
- Always read the API's documentation (rate limits, auth, endpoints).
- Use
timeoutin all calls. - Handle errors with
try/exceptandresp.raise_for_status(). - Respect rate limits. If you get 429, wait and retry.
- Don't hard-code API keys. Use environment variables.
Summary
- APIs let your Python code talk to web services using HTTP.
- The
requestslibrary makes GET and POST simple, using URL, params, headers, and JSON. - You learned how to parse JSON, handle errors, use timeouts, and work with status codes.
- You built a Mini Pokedex that fetches live data from PokeAPI and optionally downloads images.
- These skills transfer to any modern web API you'll use in school projects or real apps.