Courses/Python Mastery/Module 11: The Python Ecosystem
Module 11 · Lesson 330 minBeginner

requests: Talking to the Internet

Lesson goal
Call APIs, parse JSON responses, and handle network failures.

requests: Talking to the Internet

Every weather app, chatbot, and price tracker has the same heartbeat: ask a server, get data back. The requests library is Python's cleanest way to do it — and this lesson builds your first real API integration.

The anatomy of an API call

code
import requests

response = requests.get("https://api.github.com/users/GalvanMoto", timeout=10)
print(response.status_code)     # 200 — means "success"

Three parts:

  • `requests.get(url)` — the request: "please give me this resource"
  • `timeout=10` — never wait forever; fail politely after 10 seconds (always set this)
  • `response.status_code` — the server's answer code: 200 = OK, 404 = not found, 401 = unauthorized, 5xx = server's fault
  • JSON APIs — the standard response

    Modern APIs return JSON — which, as you know from Module 8, maps perfectly to Python dicts. requests parses it for you:

    code
    import requests
    
    response = requests.get("https://api.github.com/users/GalvanMoto", timeout=10)
    data = response.json()          # JSON → Python dict, automatically
    
    print(data["name"])             # the account's display name
    print(data["public_repos"])     # repository count
    print(list(data.keys())[:5])    # peek at what's available

    response.json() is your bridge: internet → dict → your code. Everything you learned about dictionaries applies directly.

    A real app: the GitHub lookup

    code
    import requests
    
    username = input("GitHub username: ")
    url = f"https://api.github.com/users/{username}"
    
    response = requests.get(url, timeout=10)
    
    if response.status_code == 200:
        data = response.json()
        print(f"Name:  {data.get('name', 'N/A')}")
        print(f"Repos: {data['public_repos']}")
        print(f"Followers: {data['followers']}")
    elif response.status_code == 404:
        print("No such user")
    else:
        print(f"GitHub problem: {status_code}")

    Notice the defensive shape: status check first, then parse. Parsing before checking crashes on 404s (the "user" JSON won't exist). This guard-first pattern is non-negotiable in real code.

    Sending parameters — the params argument

    Many APIs take query parameters (the ?key=value parts of URLs). Build them properly with params=:

    code
    import requests
    
    # Instead of fragile string-building:
    # requests.get("https://api.openweathermap.org/data/2.5/weather?q=Delhi&appid=KEY")
    
    response = requests.get(
        "https://api.openweathermap.org/data/2.5/weather",
        params={"q": "Delhi", "appid": "YOUR_KEY", "units": "metric"},
        timeout=10,
    )
    data = response.json()
    print(f"{data['name']}: {data['main']['temp']}°C")

    params= URL-encodes everything correctly — spaces, symbols, unicode — which manual string-building gets wrong. This is exactly the pattern from the weather app tutorial.

    Headers — identifying yourself and authenticating

    Two header uses you'll meet immediately:

    code
    # 1. A User-Agent (many APIs reject default python requests)
    headers = {"User-Agent": "MyLearningApp/1.0"}
    
    # 2. Authentication (token-based APIs — like Mistral AI)
    headers = {"Authorization": f"Bearer {api_key}"}
    
    response = requests.get(url, headers=headers, timeout=10)

    The Bearer-token pattern is how every AI API in this site's projects authenticates — the key travels in the header, never in the URL.

    The error-handling shape (production pattern)

    code
    import requests
    
    def fetch_json(url, params=None):
        try:
            response = requests.get(url, params=params, timeout=10)
            response.raise_for_status()        # raises for 4xx/5xx!
            return response.json()
        except requests.exceptions.Timeout:
            print("Server took too long")
        except requests.exceptions.HTTPError as err:
            print(f"HTTP error: {err}")
        except requests.exceptions.RequestException as err:
            print(f"Network problem: {err}")
        return None
    
    data = fetch_json("https://api.github.com/users/GalvanMoto")
    if data:
        print(data.get("name"))

    Three new tools: raise_for_status() converts bad status codes into exceptions (one check covers all 4xx/5xx), RequestException is the parent of ALL requests errors (catch it last), and returning None on failure lets the caller decide. This shape — try, raise_for_status, specific excepts, return None — is production-grade from day one.

    Common Errors & Fixes

  • `ConnectionError` / timeouts — no internet, wrong URL, or firewall. Check the URL in a browser first.
  • 401 Unauthorized — missing/wrong API key, or the header is malformed. Re-read the API's auth docs.
  • 404 — wrong URL or wrong resource ID; print the final URL you actually requested.
  • JSONDecodeError on response.json() — the API returned non-JSON (often an HTML error page). Check status_code before parsing.

  • ✅ Checkpoint

  • What does status 200 mean? 404? *(Success / not found)*
  • What parses the response body into a dict? *(response.json())*
  • Why params= instead of building the URL string? *(Correct encoding of spaces and symbols)*
  • What does raise_for_status() do? *(Raises an exception for 4xx/5xx — one check covers all bad statuses)*
  • Next: 🧪 Practice — a complete project setup from empty folder to working API call.