HTTP Requests & APIs (requests)

Reviewed & published by Brayan K

An API lets your program talk to other programs over the internet — weather data, currency rates, your own backend. The requests library makes calling them feel as easy as calling a function.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

In this lesson you'll send GET and POST requests, read JSON responses, pass parameters and headers, and handle errors like a professional.

What You'll Learn in This Lesson

1 What Is an HTTP Request?

Every time you load a webpage, your browser sends an HTTP request to a server and gets a response back. Your Python program can do exactly the same thing — but instead of HTML, APIs usually return JSON data your code can use.

🌐 The four most common HTTP methods (verbs):

First, install the library (it's not built in):

pip install requests

2 Your First GET Request

A GET request fetches data. The response object gives you the status code, the raw text, and a handy .json() method:

import requests

# A free public API for testing
response = requests.get("https://api.github.com/users/octocat")

print(response.status_code)   # 200  (200 means success)
print(response.ok)            # True (any 2xx code)

# Parse the JSON body into a Python dict
data = response.json()
print(data["login"])         # octocat
print(data["public_repos"])  # 8  (a number, varies)
print(data["name"])          # The Octocat

response.json() is the bridge to the last lesson — it runs json.loads on the response body for you and hands back a normal Python dict or list.

🚦 Status Codes — Did It Work?

Every response carries a status code. You only need to recognise the families:

RangeMeaningExamples
2xxSuccess200 OK, 201 Created
3xxRedirect301 Moved, 304 Not Modified
4xxYour mistake404 Not Found, 401 Unauthorized
5xxServer's mistake500 Server Error, 503 Unavailable

Rather than checking codes by hand, let requests raise an exception on failure:

import requests

response = requests.get("https://api.github.com/users/this-user-does-not-exist-xyz")

# Raises requests.HTTPError if status is 4xx or 5xx
response.raise_for_status()
# requests.exceptions.HTTPError: 404 Client Error: Not Found for url: ...

3 Query Parameters and Headers

Most APIs accept parameters to filter or page results. Pass a dict to params= and requests builds the URL correctly (including escaping special characters):

import requests

# Instead of manually building "?q=python&per_page=3"
response = requests.get(
    "https://api.github.com/search/repositories",
    params={"q": "python", "sort": "stars", "per_page": 3},
    headers={"Accept": "application/vnd.github+json"},
    timeout=10,
)
response.raise_for_status()

print("Requested URL:", response.url)
# https://api.github.com/search/repositories?q=python&sort=stars&per_page=3

data = response.json()
print("Total found:", data["total_count"])
for repo in data["items"]:
    print(f'{repo["full_name"]} — {repo["stargazers_count"]} stars')

4 Sending Data with POST

To create something on a server, send a POST with a JSON body. The json= argument serializes your dict and sets the right header automatically:

import requests

new_post = {
    "title": "Learning the requests library",
    "body": "It is surprisingly simple.",
    "userId": 1,
}

# jsonplaceholder is a free fake API that echoes your data back
response = requests.post(
    "https://jsonplaceholder.typicode.com/posts",
    json=new_post,
    timeout=10,
)
print(response.status_code)   # 201  (Created)

created = response.json()
print("Server assigned id:", created["id"])   # 101
print("Title:", created["title"])

Use json= for JSON bodies (the common case) and data= for form-encoded bodies (like an HTML form submission).

🛡️ Robust Requests — Timeouts & Errors

Networks fail. Servers go down. A professional request always has a timeout and a try/except around it:

import requests

def fetch_user(username):
    url = f"https://api.github.com/users/{username}"
    try:
        resp = requests.get(url, timeout=10)
        resp.raise_for_status()
        return resp.json()
    except requests.Timeout:
        print("Request timed out — server too slow")
    except requests.HTTPError as e:
        print(f"HTTP error: {e.response.status_code}")
    except requests.RequestException as e:
        print(f"Network error: {e}")
    return None   # caller can check for None

user = fetch_user("octocat")
if user:
    print("Found:", user["name"])
# Found: The Octocat
import requests

# A read-and-learn example: fetching and summarising API data.
# (Runs on your machine after: pip install requests)

resp = requests.get("https://api.github.com/users/octocat", timeout=10)
resp.raise_for_status()
user = resp.json()

print("Username:   ", user["login"])
print("Name:       ", user["name"])
print("Public repos:", user["public_repos"])
print("Followers:  ", user["followers"])

# Expected output (numbers vary over time):
# Username:    octocat
# Name:        The Octocat
# Public repos: 8
# Followers:   ~14000

Real-World Example: A Weather Lookup Function

This is what a clean, reusable API client looks like — parameters, headers, error handling and a tidy return value all in one place:

import requests

def get_weather(city, api_key):
    """Return a small dict of weather facts, or None on failure."""
    url = "https://api.openweathermap.org/data/2.5/weather"
    params = {"q": city, "appid": api_key, "units": "metric"}

    try:
        resp = requests.get(url, params=params, timeout=10)
        resp.raise_for_status()
    except requests.RequestException as e:
        print(f"Could not reach weather service: {e}")
        return None

    data = resp.json()
    return {
        "city": data["name"],
        "temp": data["main"]["temp"],
        "feels_like": data["main"]["feels_like"],
        "description": data["weather"][0]["description"],
    }

# Usage (you'd get a free key from openweathermap.org):
weather = get_weather("London", api_key="YOUR_KEY_HERE")
if weather:
    print(f"{weather['city']}: {weather['temp']}C, {weather['description']}")
# Example output:
# London: 12.4C, light rain

Notice how the messy nested JSON (data["main"]["temp"]) gets flattened into a clean dict the rest of your program can use without knowing the API's structure.

import requests

# POST example using a free fake API that echoes data back.
# (Runs on your machine after: pip install requests)

payload = {"title": "My First Post", "body": "Hello API!", "userId": 7}

resp = requests.post(
    "https://jsonplaceholder.typicode.com/posts",
    json=payload,
    timeout=10,
)
resp.raise_for_status()

result = resp.json()
print("Status code:", resp.status_code)   # 201
print("New record id:", result["id"])     # 101
print("Echoed title:", result["title"])   # My First Post

# Expected output:
# Status code: 201
# New record id: 101
# Echoed title: My First Post
# 🎯 YOUR TURN — replace each ___ using the hint beside it.
#
# No network here on purpose: every piece below is the part you get wrong,
# and none of it needs a server to practise.

import json
from urllib.parse import urlencode, urlparse, parse_qs

base = "https://api.example.com/search"

# 1) Escapes spaces and specials properly — never build a query by hand.
query = ___({"q": "python books", "page": 2})   # 👉 replace ___ with urlencode
url = f"{base}?{query}"
print("URL:", url)

# 2) Take a URL apart into scheme, host, path and query.
parts = ___(url)                                # 👉 replace ___ with urlparse
print("Host:", parts.netloc, "Path:", parts.path)
print("Parsed query:", parse_qs(parts.query))

body = '{"count": 2, "results": [{"title": "Fluent Python"}, {"title": "Dune"}]}'

# 3) Text to Python. (dumps goes the other way — easy to mix up.)
data = json.___(body)                           # 👉 replace ___ with loads
print("Count:", data["count"])
for item in data["results"]:
    print(" -", item["title"])

payload = {"title": "New Book", "pages": 300}
# 4) Python to text, with the keys in a fixed order so it is comparable.
print("Sending:", json.___(payload, sort_keys=True))   # 👉 replace ___ with dumps

status = 404
if 200 <= status < 300:
    print("OK")
# 5) The one that means the server is fine and the path is not.
elif status == ___:                             # 👉 replace ___ with 404
    print("Nothing there — check the path before retrying")
else:
    print("Something else went wrong")

# ✅ Expected output:
# URL: https://api.example.com/search?q=python+books&page=2
# Host: api.example.com Path: /search
# Parsed query: {'q': ['python books'], 'page': ['2']}
# Count: 2
#  - Fluent Python
#  - Dune
# Sending: {"pages": 300, "title": "New Book"}
# Nothing there — check the path before retrying

🧩 Reorder Challenge

These lines fetch an API and print one field, but they're scrambled. Find the correct order:

A) resp.raise_for_status()
B) import requests
C) print(resp.json()["login"])
D) resp = requests.get(url, timeout=10)
E) url = "https://api.github.com/users/octocat"

Correct order: B → E → D → A → C

import requests
url = "https://api.github.com/users/octocat"
resp = requests.get(url, timeout=10)
resp.raise_for_status()
print(resp.json()["login"])    # octocat

Import, define the URL, make the request, check for errors before trusting the body, then parse and use it.

🧠 Quick Recall — Predict the Output

# A request returned status_code 200. What does this print?
resp_ok = True   # response.ok for a 200
print("Success" if resp_ok else "Failed")

Success — a 200 status makes response.ok True (any 2xx code is OK).

# Which argument sends a JSON body in a POST?
opts = ["data=", "json=", "params="]
print(opts[1])

json= — passing json=your_dict serializes it and sets the Content-Type to application/json. params= is for URL query strings; data= is for form-encoded bodies.

status = 404
family = status // 100
print(f"{family}xx — client error" if family == 4 else "other")

4xx — client error — 404 // 100 is 4, the "your mistake" family (resource not found).

❓ Frequently Asked Questions

Your programs can now talk to the whole internet!

You can send GET and POST requests, pass params and headers, read JSON responses, and handle timeouts and errors gracefully. APIs are how modern software connects — this skill opens thousands of doors.

Practice quiz

What does response.json() do?

  • Saves the response to a .json file
  • Parses the JSON body into a Python dict or list
  • Converts a dict to a JSON string
  • Validates the JSON schema

Answer: Parses the JSON body into a Python dict or list. It runs json.loads on the response body and hands back a normal Python dict or list.

Which HTTP method fetches data (read) from an API?

  • GET
  • POST
  • PUT
  • DELETE

Answer: GET. GET retrieves data; POST creates, PUT/PATCH update, DELETE removes.

A response with status_code 200 makes response.ok equal to what?

  • False
  • True
  • None
  • 200

Answer: True. Any 2xx status (including 200) makes response.ok True.

Which argument sends a JSON body in a POST request?

  • data=
  • params=
  • json=
  • body=

Answer: json=. json=your_dict serializes it and sets Content-Type to application/json. params= is for query strings.

What does the params= argument do in requests.get?

  • Sends a JSON body
  • Builds and escapes the URL query string
  • Sets request headers
  • Adds a timeout

Answer: Builds and escapes the URL query string. params={'q': 'python'} becomes ?q=python in the URL, with special characters escaped for you.

What status family is a 404?

  • 2xx — success
  • 3xx — redirect
  • 4xx — client error
  • 5xx — server error

Answer: 4xx — client error. 404 // 100 is 4 — the 'your mistake' client-error family (resource not found).

What does response.raise_for_status() do on a 404 response?

  • Returns False
  • Raises requests.HTTPError
  • Retries the request
  • Returns the JSON

Answer: Raises requests.HTTPError. It raises an HTTPError for any 4xx or 5xx status, turning failures into catchable exceptions.

Why should every request include timeout=?

  • It makes requests faster
  • Without it, a stuck server can hang your program forever
  • It is required syntax
  • It caches the response

Answer: Without it, a stuck server can hang your program forever. A timeout makes a slow/unresponsive server fail cleanly instead of freezing your script.

Which exception is the parent of Timeout, ConnectionError, and HTTPError?

  • requests.RequestException
  • requests.Error
  • Exception
  • requests.HTTPError

Answer: requests.RequestException. requests.RequestException is the base class, making it a safe catch-all in a try/except.

A successful POST that creates a resource typically returns which status code?

  • 200 OK
  • 201 Created
  • 204 No Content
  • 400 Bad Request

Answer: 201 Created. 201 Created signals that the server made a new resource (e.g. jsonplaceholder returns id 101).

Continue this course

Frequently asked questions

What is the requests library?

requests is the most popular third-party Python library for making HTTP calls. It wraps the lower-level urllib with a clean, human-friendly API. Install it with: pip install requests.

What is the difference between GET and POST?

GET retrieves data and puts parameters in the URL. POST sends data to the server (e.g. submitting a form or creating a record) in the request body. There are also PUT, PATCH and DELETE for updating and removing resources.

What does response.raise_for_status() do?

It checks the HTTP status code and raises an HTTPError exception if the request failed (status 4xx or 5xx). It's the cleanest way to turn a failed request into a catchable error instead of silently continuing.

How do I send JSON in a POST request?

Pass your dict to the json= parameter: requests.post(url, json=data). requests automatically serializes it and sets the Content-Type header to application/json.

Why should I always set a timeout?

Without a timeout, a slow or unresponsive server can make your program hang forever. Always pass timeout=10 (seconds) so a stuck request fails cleanly instead of freezing.