Chapter 3: Your First API Call

Connecting to the Internet with Python

1. The Moment You've Been Waiting For

In the next few minutes, you’re going to fetch live data from the internet with Python. Not a simulation and not a pre-baked tutorial dataset, but real information from actual servers, the same way Instagram loads your feed or your weather app shows today’s forecast.

This is the moment where everything starts to click. You’ve installed Python, set up your environment, and learned the core ideas. Now you’ll see APIs in action, in real time. By the end of this chapter, you’ll have made dozens of successful requests and you’ll understand, step by step, how connected applications talk to servers.

Every time you open a weather app, scroll social media, or check your bank balance, your device is quietly making API calls. These conversations happen millions of times per second across the internet. In this chapter, you join the developers who build those conversations on purpose, instead of just watching them happen.

Learning Objectives

What You'll Master in This Chapter

By the end of this chapter, you’ll be able to:

  • Make your first API request in Python using the requests library.
  • Explain, in plain language, how APIs connect your programs to real data on servers.
  • Read and interpret JSON responses from APIs.
  • Handle basic errors when network requests fail instead of letting your program crash.
  • Use query parameters to customize and refine your API requests.
  • Build a complete program that integrates multiple APIs into one script.

2. Making Your First API Call

The best way to understand APIs is to see them in action. We’ll start with something simple: calling an API directly in your browser, then repeating the same call in Python. The Cat Facts API is a small, free service that returns random facts about cats. When you visit its URL in your browser, you’re seeing exactly what the server sends back.

Access An API In Your Browser

Before writing any Python code, let’s see what an API response actually looks like. Open this URL in your browser. This is you making an API request manually:

Browser
https://catfact.ninja/fact

You’ll see a block of raw text in a format called JSON. It might look a little dense at first, but it’s just structured text that your computer can read easily. Your browser sent a request to the server, and the server responded with this JSON. A Python program would receive the very same data. Under the hood, almost all API responses look like this: structured text that code can parse and work with.

Now Try It in Python

Now let’s make that exact same request using Python. You’re going to write a few lines that connect to a live server, send a request, and print the data that comes back. Notice how little code you need-the requests library hides all the low-level networking details for you.

Your First API Request
Python
import requests

url = "https://catfact.ninja/fact"
response = requests.get(url)        
# You sent a request to a server
# The server sent back data

print(response.text)
# You're seeing that data                
Output (example)
{"fact":"Cats have five toes on their front paws, but only four on the back ones.","length":70}

In just a few lines of code, you connected to a live server on the internet and received real data back. This is exactly how your phone’s apps talk to servers-the only difference is that you’re now in control of the conversation.

What Just Happened

Let’s break down each line:

  • import requests - Load the library that knows how to talk to web servers.
  • requests.get(url) - Send a request to the server at that URL, asking for data.
  • response.text - Access the raw text the server sent back.
    Note: This output looks like a Python dictionary, but right now it is just a dumb string of text. In a moment, we will turn it into real data.

Your computer packaged up an HTTP request, sent it across the internet to catfact.ninja’s servers, waited for the response, and received structured data back-all in a fraction of a second. This is the basic pattern you’ll reuse, refine, and harden throughout the rest of the book.

3. Understanding the Response Object

When you call a REST API, the response object is everything your code gets back from the server wrapped in a convenient package. It captures both what happened and what was returned.

  • Status code: Tells you whether the request succeeded or failed (for example, 200, 404, 500).
  • Headers: Provide extra metadata about the response (content type, rate-limit info, caching hints, and more).
  • Body: Contains the actual data you care about (often JSON), which you can read as text or parse into native objects.

Instead of you manually parsing raw HTTP text, your HTTP client library turns the response into a structured object with properties and methods, so you can quickly check the status, inspect headers, read the body, and convert it into native data structures.

Let’s break down the main components in more detail.

Key Components of a Response Object

1. Status Information

This tells you whether the request succeeded or failed. The status includes a numeric code (like 200 or 404) and a short text description (like "OK" or "Not Found").

You can access the status information using these attributes:

  • response.status_code - Numeric code (200, 404, 500, etc.)
  • response.reason - Human-readable phrase ("OK", "Not Found", "Internal Server Error")
  • response.ok - Boolean, True if status code is between 200 and 399

2. Headers

These are key-value pairs of metadata about the response. Headers tell you things like what type of content was sent (Content-Type: application/json), when it was sent (Date), how large it is (Content-Length), and other information about how to handle the response.

You can access the headers using this attribute:

  • response.headers - Dictionary-like object containing all headers
  • Common headers: Content-Type, Content-Length, Date, Server

3. Body

This is the actual data you requested-the payload the server sends back. The body might be JSON from an API, HTML from a webpage, or the raw bytes of an image or PDF file. Your HTTP client lets you access this same body in different formats depending on what you need:

You can access the body using these attributes:

  • response.content - Raw bytes (for images, PDFs, binary files)
  • response.text - Decoded string (for HTML, CSV, plain text)
  • response.json() - Parsed Python dictionary or list (for JSON API data)

Three Types Of Content

JSON, Text and Raw Data

When you drill into the body of a response, you’re looking at the same data through different “lenses.” Most REST APIs give you one of three useful views:

  • JSON: Structured data you can work with as Python dictionaries and lists.
  • Text: Human-readable content like HTML, CSV, or plain text.
  • Raw bytes: Binary data like images, PDFs, and other files.

Your HTTP client exposes each of these through a different attribute or method, so you can choose the representation that best fits what you’re trying to do.

What Is JSON?

The Language APIs Use to Send Data

JSON (JavaScript Object Notation) is a lightweight text format for representing structured data. Almost every modern web API uses JSON because it’s easy for humans to read and easy for computers to parse.

  • Human-readable: You can scan it and understand the shape of the data.
  • Machine-friendly: Libraries can parse it into native objects in one step.
  • Language-agnostic: The same JSON works in Python, JavaScript, Java, and more.

When you look at it, JSON often feels similar to a Python dictionary. It uses curly braces, key–value pairs, and nested structures to describe data. When you make an API request, the response body arrives as JSON text that you convert into Python data structures using the response.json() method.

The format feels immediately familiar to Python developers because JSON mirrors Python's basic containers.

  • A JSON "object" maps naturally to a Python dictionary
  • A JSON "array" maps naturally to a Python list
  • The syntax is nearly identical-curly braces, colons, commas in the same places

Here's the same data in both formats:

JSON (what arrives from the API)

Here is a JSON object, containing fields, one of which has an array.

JSON
{
  "name": "Luna",
  "age": 3,
  "is_cat": true,
  "toys": ["mouse", "string"],
  "owner": null
}
Python (after parsing with response.json())

Here is a Python dictionary, containing key/value pairs, one of which has a list.

Python
{
    "name": "Luna",
    "age": 3,
    "is_cat": True,
    "toys": ["mouse", "string"],
    "owner": None
}

This similarity is why working with APIs in Python feels natural. Once the JSON text is parsed, you're navigating familiar dictionaries and lists using bracket notation and key lookups. The methods you already know-data.get(), for item in data['results'], if 'key' in data-work exactly the same way.

Terminology

Just be aware: API documentation uses JSON terminology, but these map directly to Python types you already understand:

JSON Term Python Term Example
Object Dictionary (dict) { "id": 1 }
Array List [1, 2, 3]
Field Key/Value Pair (Item) "name": "Luna"

In the next subsection, you'll see response.json() in action-how it converts that text string from the API into the Python dictionaries and lists you'll work with throughout this book.

Parsing JSON

Transforming API Responses into Python Data

Now that you've seen how JSON maps onto Python's own data structures, let's put it to work with a real API response.

In practice, parsing JSON is usually two steps:

  • Call response.json() to convert the JSON text into Python objects.
  • Extract the fields you care about using dictionary keys and list indexing.

In the example below, we’ll reuse the cat fact API, parse the JSON, and pull out two values: the fact itself and its length.

Extracting Data from JSON Responses
Python
import requests

response = requests.get("https://catfact.ninja/fact", timeout=10)

print(response.status_code)
print("-" * 30)

# .json() does the work of converting string -> dict
data = response.json()

print("Fact:", data["fact"])
print("Length:", data["length"])
Output (example)
200
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
Fact: Cats must have fat in their diet because they can't produce it on their own.
Length: 76
What We Did
  • Sent a GET request to the API
  • Received a Response object back from the server
  • Parsed the JSON body into a Python dictionary using response.json()
  • Accessed individual values with dictionary keys: data["fact"], data["length"]

The response.json() method does all the hard work of converting JSON text into Python data structures. What you get back is a regular Python dictionary that you can work with using all your existing knowledge.

4. httpbin.org: Your API Playground

When you're learning APIs, it helps to have a safe practice server that always behaves predictably. httpbin.org is exactly that: a free service built specifically for experimenting with HTTP. You can send it requests of all kinds, and it will simply show you, in plain JSON, what it received and how it interpreted your request.

In day-to-day work, developers use httpbin.org as a kind of Swiss Army knife for HTTP. It’s where you go when you want to:

  • See exactly what headers your client is sending
  • Inspect query parameters and request metadata
  • Understand what cookies look like in requests and responses
  • Test timeouts and slow responses with /delay
  • Feel what different status codes look like in your code

Because it’s simple, predictable, and doesn’t require an account or API key, it’s perfect for experimenting and debugging without worrying about breaking anything.

In the rest of this chapter, you’ll use httpbin.org as your practice server while you build up real confidence with requests and responses.

Examples You'll Use

  • httpbin.org/get - Returns the details of a GET request (headers, query parameters, URL)
  • httpbin.org/status/404 - Returns a custom status code (like 404 Not Found)
  • httpbin.org/json - Returns sample JSON data
  • httpbin.org/delay/3 - Waits 3 seconds before responding (useful for testing timeouts)
  • httpbin.org/image/png - Returns an image you can download or save to a file

Let's make your first request to httpbin. This will show you exactly what information your Python program sends when it makes a request.

The https://httpbin.org/get URL (often referred to as the /get endpoint on the httpbin.org server) simply echoes back everything it received:

  • Headers: what your client sent automatically
  • Origin IP: where the request came from
  • Final URL: including any query parameters

It’s like holding up a mirror to your HTTP request so you can see it from the server’s point of view.

Your First httpbin.org Request
Python
import requests

url = "https://httpbin.org/get"
response = requests.get(url, timeout=5)

print("Status:", response.status_code)
print("JSON:", response.json())
Output (example)
Status: 200
JSON: {
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "User-Agent": "python-requests/2.31.0"
  },
  "origin": "203.0.113.45",
  "url": "https://httpbin.org/get"
}
Understanding the Response

httpbin.org is simply showing you what it received from your request:

  • args - Any query parameters you sent in the URL (empty in this example)
  • headers - Extra information your client included automatically (such as what software you’re using)
  • origin - The IP address your request came from (how the server knows where you are)
  • url - The exact URL that was requested

Notice the User-Agent header. Your client automatically told the server, “I’m using python-requests version 2.31.0.” Every browser and HTTP library sends its own user agent string. Servers can use this information for logging, analytics, or sometimes to adjust how they respond to different types of clients.

5. Always Use Timeout

You might have noticed timeout=5 in the previous example. That small argument is not a nice-to-have; it's essential. Without a timeout, your program is willing to wait forever if a server never responds. “Forever” here is literal: that line of code will sit there blocked until you manually stop the script or restart the process.

By adding timeout=10, you’re telling requests: “Wait up to 10 seconds for this server to respond, then give up and raise an error.”

  • Without a timeout: your program can hang indefinitely.
  • With a timeout: your program fails fast and you can handle the exception.

Let’s start with a request that completes successfully with a timeout in place, then we’ll compare it to a request that deliberately takes too long.

Protected Request
Python
import requests

response = requests.get("https://httpbin.org/get", timeout=10)
print("Request completed successfully!")
print(f"Status code: {response.status_code}")
print(f"Response type: {type(response)}")
Output
Request completed successfully!
Status code: 200
Response type: <class 'requests.models.Response'>

The timeout=10 parameter tells requests to wait at most 10 seconds for the server to respond. If the server is slower than that, requests raises a Timeout exception instead of leaving your program stuck. That small safeguard makes a big difference to the reliability of your code.

Understanding Timeout Behavior

Timeouts protect your application from hanging, but they also remind you that network calls live in the real world, not in a perfectly reliable lab:

  • Local networks: Usually fast (milliseconds); timeouts are rarely triggered
  • Internet services: Performance varies (50–500ms typical); occasional delays are normal
  • Overloaded servers: May respond slowly or not at all during high traffic
  • Network congestion: Can introduce delays anywhere along the route

Professional applications expect these conditions and handle them gracefully rather than crashing or hanging.

Seeing a Timeout Error in Action

Let’s deliberately trigger a timeout so you can see what it looks like in practice. httpbin.org has a /delay/N endpoint that waits N seconds before responding, which is perfect for testing timeout behavior. We’ll ask the server to wait 15 seconds, but we’ll only give it 5 seconds before we give up. This demonstrates exactly how timeout protection works:

Timeout Error Demonstration
Python
import requests

# This server will delay for 15 seconds, but we'll only wait 5
response = requests.get("https://httpbin.org/delay/15", timeout=5)
Error Output
Traceback (most recent call last):
  File "test.py", line 4, in <module>
    response = requests.get("https://httpbin.org/delay/15", timeout=5)
  ...
requests.exceptions.ReadTimeout: HTTPSConnectionPool(host='httpbin.org', port=443): 
Read timed out. (read timeout=5)

This timeout error is actually protecting your program. The message is telling you that the server did not respond within 5 seconds, so requests stopped waiting and raised an exception. That is exactly what you want in a real application: fail clearly and quickly rather than hang silently in the background.

Timeout Best Practices
  • Always include timeout: Every requests.get() (and other HTTP methods) should have a timeout parameter.
  • Choose reasonable values: 5–10 seconds works for most APIs; use 30 seconds or more only for truly slow operations.
  • Handle timeout errors: Use try/except blocks (covered in Section 7) to catch these exceptions and react appropriately.
  • Test timeout behavior: Use httpbin.org/delay while developing to verify that your code handles slow responses gracefully.

In Chapter 9, you'll learn more sophisticated error handling that turns timeout errors into user-friendly messages. For now, just remember: timeout parameters are not optional-they're essential for writing robust API code.

6. Inspecting the Response Object

Before you start pulling data out of a response, pause and make sure the request actually succeeded. The Response object from the requests library contains everything you need to check:

  • Status: did the request succeed (200) or fail (404, 500)?
  • Headers: what format did the server send back?
  • Body: what data did you actually receive?

Professional developers don’t assume success. They inspect first, then write parsing code against what the server really returned.

Let’s examine a Response object systematically. We’ll make a request to httpbin’s /json endpoint, and then inspect the object to see what information is available. As you read through the output, notice how much metadata comes back in addition to the JSON itself:

Quick Inspection
Python
import requests

response = requests.get("https://httpbin.org/json", timeout=10)

print(response)                  # Quick summary
print(f"Status Code: {response.status_code}")
print(f"Request Successful: {response.ok}")
print(f"Object Type: {type(response)}")
Output
<Response [200]>
Status Code: 200 
Request Successful: True
Object Type: <class 'requests.models.Response'>
What This Tells Us
  • <Response [200]> - Python’s string representation of the Response object. The angle brackets show it’s an object, and [200] is the status code.
  • status_code = 200 - The request succeeded (200 is the standard “OK” code).
  • ok = True - A quick boolean check: True for 2xx status codes, False for error codes.
  • type(response) - Confirms we’re working with a Response object from the requests library.

Understanding Status Codes

Status codes are three-digit numbers that tell you what happened with your request. They’re standardized across the web, so every API uses the same codes to mean the same things. Once you know the common ones, you can glance at a status code and immediately tell whether your request worked, failed, or needs a follow-up.

Code Meaning What It Tells You
200 OK Success! The request worked and data was returned.
404 Not Found The URL or resource doesn’t exist on the server.
500 Internal Server Error The server crashed or has a bug; this is not your fault.
429 Too Many Requests You’re making requests too quickly; slow down or add delays.
401 Unauthorized You need to provide credentials (API key, login, token).
503 Service Unavailable The server is temporarily down or overloaded; try again later.
Status Code Ranges

Status codes follow a pattern based on their first digit:

  • 2xx (Success): Everything worked as expected.
  • 4xx (Client Error): Something is wrong with your request (bad URL, missing auth, invalid data).
  • 5xx (Server Error): Something went wrong on the server (usually not your fault).

The response.ok property returns True for any 2xx status code and False for everything else. This gives you a quick success check without memorizing every individual status code.

Detailed Response Analysis

Beyond status codes, Response objects also carry headers: small pieces of metadata that provide extra context about the response. Headers tell you things like what format the data is in, when it was generated, and what server software sent it. You won’t read headers on every request, but when something feels “off,” they’re one of the first places to look.

Let’s write a more comprehensive inspection script that displays not just the status, but also key headers and other metadata. This is the kind of diagnostic code you might write when troubleshooting tricky API integration issues:

Complete Response Inspection
Python
import requests

response = requests.get("https://httpbin.org/get", timeout=10)

# Examine the response object structure
print("Response Object Analysis")
print("=" * 50)
print(f"Object type: {type(response)}")
print(f"Status code: {response.status_code}")
print(f"Status text: {response.reason}")
print(f"Success check: {response.ok}")
print(f"Content length: {len(response.text)} characters")
print(f"Response URL: {response.url}")

# Show key headers
print("\nKey Response Headers:")
print(f"Content-Type: {response.headers.get('Content-Type')}")
print(f"Server: {response.headers.get('Server')}")
print(f"Date: {response.headers.get('Date')}")
Sample Output
Response Object Analysis
==================================================
Object type: <class 'requests.models.Response'>
Status code: 200
Status text: OK
Success check: True
Content length: 321 characters
Response URL: https://httpbin.org/get

Key Response Headers:
Content-Type: application/json
Server: gunicorn/19.9.0
Date: Sat, 06 Sep 2025 21:10:00 GMT
What This Tells Us
  • status_code and reason - Numeric (200) and text (OK) indicators of success.
  • ok - A quick True/False shortcut for 2xx success codes.
  • url - The final URL used (especially useful if redirects occurred).
  • Content-Type - Tells you the format of the response (application/json, text/html, image/png, and so on).
  • Server - Identifies what software the server is running (interesting, though rarely essential).
  • Date - When the server generated the response.

You won’t inspect headers on every request, but when something doesn’t look right, they’re invaluable. For example, if you expect JSON but get HTML, checking the Content-Type header will immediately confirm whether the server actually sent JSON or something else.

Developer Habit: Inspect First, Trust Later

When you call a new API for the first time, don’t jump straight into parsing the JSON. Make a habit of inspecting the raw response first:

  • Print response.status_code to confirm the request actually succeeded.
  • Print a slice of response.text (for example, the first 300 characters) to see what the server really sent.
  • Check response.headers.get("Content-Type") to verify the format (JSON, HTML, image, etc.).

Once you know the status, format, and general shape of the data, then it’s time to write parsing code. This simple “inspect first” habit will save you a huge amount of debugging time as your projects grow.

7. Basic Error Handling

Not every API call will succeed. Common failures include:

  • Servers go down or return errors
  • Wi-Fi drops or networks fail mid-request
  • URLs have typos or endpoints change
  • Requests time out when services are slow

If you don’t handle these situations, your program will crash the moment something goes wrong. Professional developers expect failures and build in error handling so their applications can respond gracefully instead of blowing up.

First, let’s see what happens when things go wrong and there’s no protection in place. We’ll deliberately request a URL that returns a 404 error (Not Found), then call response.raise_for_status(). Notice how the program stops at the error and never reaches the code that comes afterwards:

What Happens Without Error Handling
Python - Without Protection
import requests

response = requests.get("https://httpbin.org/status/404", timeout=5)
response.raise_for_status()  # This will crash the program
print("This line never executes")
Error Output
Traceback (most recent call last):
  File "test.py", line 4, in <module>
    response.raise_for_status()
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://httpbin.org/status/404

The program crashed with an HTTPError. The print() statement never executed because Python stopped as soon as raise_for_status() raised the exception. Now let’s add basic error handling so we can catch this exception and respond more gracefully:

With Error Handling
Python - Protected
import requests

try:
    response = requests.get("https://httpbin.org/status/404", timeout=5)
    response.raise_for_status()
    print("Success!")
except requests.exceptions.HTTPError as e:
    print(f"HTTP error occurred: {e}")
except requests.exceptions.Timeout:
    print("Request timed out")
except requests.exceptions.RequestException as e:
    print(f"An error occurred: {e}")
Output
HTTP error occurred: 404 Client Error: Not Found for url: https://httpbin.org/status/404

Now, instead of crashing, the program catches the error, prints a clear message, and can continue running. This is the difference between a fragile script and code that’s on its way to being production-ready.

Common Request Exceptions
  • HTTPError - The server returned an error status code (4xx or 5xx).
  • Timeout - The server didn’t respond within the timeout period.
  • ConnectionError - A network problem prevented the connection from being made.
  • RequestException - The base exception that catches any requests-related error.

By catching these specific exceptions, you can provide tailored, helpful messages for different failure scenarios instead of a generic crash.

Practicing with Different Status Codes

httpbin.org lets you simulate any HTTP status code using the /status/CODE endpoint. This is perfect for testing how your error handling behaves in different scenarios. Let’s create a small helper function that tries several status codes and prints how each one is handled. This kind of experimentation teaches you what to expect in real-world API usage:

Testing Different Status Codes
Python
import requests

def test_status_code(code):
    """Test how your code handles different status codes."""
    try:
        url = f"https://httpbin.org/status/{code}"
        response = requests.get(url, timeout=5)
        response.raise_for_status()
        print(f"✓ Status {code}: Success")
    except requests.exceptions.HTTPError as e:
        print(f"✗ Status {code}: {e}")

# Test different scenarios
test_status_code(200)  # Success
test_status_code(404)  # Not Found
test_status_code(500)  # Server Error
test_status_code(429)  # Rate Limited
Output
✓ Status 200: Success
✗ Status 404: 404 Client Error: Not Found for url: https://httpbin.org/status/404
✗ Status 500: 500 Server Error: Internal Server Error for url: https://httpbin.org/status/500
✗ Status 429: 429 Client Error: Too Many Requests for url: https://httpbin.org/status/429

This kind of hands-on testing builds confidence. You see exactly how different status codes behave and what exceptions they raise. Later, when you encounter these codes in real APIs, you’ll already know what they mean and have a pattern for how to handle them.

8. Query Parameters

Most real-world APIs need extra details in your request. When asking for weather data, you specify the location. When searching for videos, you provide search terms. When filtering results, you include your criteria. These extra pieces of information are sent as query parameters.

Query parameters are added to URLs after a ?, written as key=value pairs, and separated by &. They provide additional information that customizes the API’s response. Let’s break down the anatomy of a URL that uses query parameters:

URL Anatomy
URL Structure
https://httpbin.org/get?name=Alice&age=25&city=Boston
└──────────┬──────────┘ └──────────────┬──────────────┘
      Base URL                    Query Parameters

Breaking it down:
  ?name=Alice    ← First parameter (starts with ?)
  &age=25        ← Second parameter (starts with &)
  &city=Boston   ← Third parameter (starts with &)

To see how this works in practice, we’ll use httpbin.org’s /get endpoint. It echoes back whatever parameters you send, making it perfect for understanding how servers receive your data. Let’s send some parameters and look at exactly what the server reports back:

Sending Parameters in a URL
Python
import requests

url = "https://httpbin.org/get?name=Alice&age=25&city=Boston"
response = requests.get(url, timeout=5)

print("Status:", response.status_code)
print("\nWhat the server received:")
data = response.json()
print("Parameters:", data["args"])
Output
Status: 200

What the server received:
Parameters: {'name': 'Alice', 'age': '25', 'city': 'Boston'}

The server parsed the query string and extracted each parameter. Notice that age became a string ("25") even though you might think of it as a number. Query parameters are always transmitted as text; it’s up to the server to decide whether to interpret them as numbers, dates, or other types.

How Query Parameters Work
  • Base URL: The main endpoint (https://httpbin.org/get)
  • Question mark (?): Marks the start of the query string
  • Key-value pairs: Written as key=value
  • Ampersand (&): Separates multiple parameters
  • All values are strings: Even numbers are transmitted as text

9. Using the params Argument

Manually building parameter strings is error-prone. What if a value contains spaces? Special characters? Symbols like & or =? The params argument in the requests library solves these problems by automatically handling URL encoding, type conversion, and formatting. It feels like a small convenience-until you run into the subtle bugs that manual string building can cause.

Why Use params?

Passing parameters as a dictionary is cleaner, safer, and more maintainable than string concatenation:

  • Automatic URL encoding: Handles spaces, special characters, and symbols correctly.
  • Type conversion: Converts Python types (int, float, bool) to strings automatically.
  • Cleaner code: No manual string building or ampersand juggling.
  • Error prevention: Much harder to accidentally break the URL with formatting mistakes.

Let’s compare the manual approach (fragile) with the params approach (robust). This will show you why you should reach for params by default:

Manual vs. params Comparison
Manual (Don't Do This)
import requests

# Manual string building - fragile and error-prone
name = "Alice Smith"  # What if there's a space?
city = "New York"     # What about special characters?
url = f"https://httpbin.org/get?name={name}&city={city}"  # Easy to get wrong

response = requests.get(url, timeout=5)
Using params (Do This)
import requests

# Clean, safe parameter passing
params = {
    "name": "Alice Smith",
    "city": "New York",
    "age": 25
}

response = requests.get("https://httpbin.org/get", params=params, timeout=5)

The params version automatically handles the spaces in "Alice Smith" and "New York" by encoding them safely in the URL. You don’t have to think about the low-level details; requests takes care of it.

Now let’s look at a complete example that shows how params handles multiple data types and what URL it actually constructs. Pay attention to the “Final URL” in the output-this is what params built for you automatically:

Complete params Example
Python
import requests

url = "https://httpbin.org/get"

# Use a dictionary for parameters
params = {
    "name": "Alice Smith",
    "age": 25,
    "city": "New York",
    "verified": True
}

response = requests.get(url, params=params, timeout=5)

print("Status:", response.status_code)
print("Final URL:", response.url)
print("\nWhat the server received:")
data = response.json()
for key, value in data["args"].items():
    print(f"  {key}: {value}")
Output
Status: 200
Final URL: https://httpbin.org/get?name=Alice+Smith&age=25&city=New+York&verified=True

What the server received:
  name: Alice Smith
  age: 25
  city: New York
  verified: True
Key Points
  • Automatic encoding: "Alice Smith" became Alice+Smith in the URL (+ is another way to encode spaces).
  • Type conversion: The integer 25 and boolean True were converted to strings automatically.
  • response.url: Shows the complete URL that was actually requested, including encoded parameters.
  • Server sees decoded values: The server receives "Alice Smith" (not "Alice+Smith"); the encoding is just for safe transmission.
Best Practice

Always use the params argument instead of manually building query strings. It’s more reliable, more readable, and handles edge cases you might not think of. The only time you might use a fully hard-coded URL is when you’re copying an exact example from documentation, and even then, you can usually translate it into a params dictionary for clarity.

10. Mini Project: Cat Facts and Jokes

Now let’s put everything you’ve learned in this chapter to work in a small but complete program. You’ll:

  • Fetch data from two public APIs
  • Use timeouts and handle potential errors
  • Parse JSON and extract the fields you care about
  • Present the results in a user-friendly format

This is the complete API workflow end to end:

  • Make a request
  • Check for errors
  • Parse JSON
  • Display results

As you read the code, pay attention to how it’s organized into functions. This is the kind of structure you’ll use in real applications.

Random Cat Fact and Joke Generator
Python
"""
Cat Facts and Jokes
A simple program that fetches random data from two different APIs.
"""

import requests

def get_cat_fact():
    """Fetch a random cat fact from catfact.ninja."""
    try:
        url = "https://catfact.ninja/fact"
        response = requests.get(url, timeout=5)
        response.raise_for_status()
        return response.json()["fact"]
    except requests.exceptions.RequestException as e:
        return f"Couldn't fetch cat fact: {e}"

def get_joke():
    """Fetch a random joke from official-joke-api."""
    try:
        url = "https://official-joke-api.appspot.com/random_joke"
        response = requests.get(url, timeout=5)
        response.raise_for_status()
        data = response.json()
        return f"{data['setup']} ... {data['punchline']}"
    except requests.exceptions.RequestException as e:
        return f"Couldn't fetch joke: {e}"

# Main program
print("=" * 50)
print("🐱 CAT FACTS & JOKES 🤣")
print("=" * 50)

print("\nHere's a cat fact for you:")
print(get_cat_fact())

print("\nAnd here's a joke:")
print(get_joke())

print("\n" + "=" * 50)
Output (example)
==================================================
🐱 CAT FACTS & JOKES 🤣
==================================================

Here's a cat fact for you:
Cats sleep for 70% of their lives, which means a 9-year-old cat 
has been awake for only three years of its life.

And here's a joke:
Why don't scientists trust atoms? ... Because they make up everything!

==================================================
What You Learned
  • Multi-API integration: Called two different APIs from the same script.
  • Function organization: Separated concerns into dedicated functions for each API.
  • Error handling: Each function handles its own network and HTTP errors gracefully.
  • JSON parsing: Extracted specific fields from different JSON response shapes.
  • User experience: Formatted output so it’s friendly and readable, not just raw JSON.

Your Turn: Extend This Program

Practice is where this really sticks. Try these small challenges to extend the program and test your understanding:

1.

Add User Choice

Let users choose whether they want a cat fact or a joke using input(). Only display the option they ask for.

2.

Add a Third API

Integrate https://uselessfacts.jsph.pl/random.json?language=en to show random facts. Parse the JSON and extract the text field.

3.

Create a Loop

Wrap everything in a while True loop so users can request multiple facts and jokes. Add a "quit" option to exit gracefully.

4.

Add Parameters

Modify get_cat_fact() to accept a max_length parameter. Use https://catfact.ninja/fact?max_length=50 (via params) to request shorter facts.

Challenge Solutions

Don’t peek at solutions immediately. Try each challenge first. The struggle is where the learning happens. If you get stuck for more than 15 minutes, check the chapter resources or ask for help, but always make a serious attempt before looking up an answer.

What Makes This Code Production-Ready

This mini-project isn’t just a toy example; it demonstrates patterns you’ll see in professional applications. Let’s highlight what makes it “production-style” code:

  • Timeout on every request: No hanging calls, no infinite waits.
  • Comprehensive error handling: Network issues and HTTP errors are caught and reported clearly.
  • Function separation: Each API call lives in its own function, which makes it easier to test and reuse.
  • Clear documentation: Docstrings explain what each function does at a glance.
  • User-friendly output: The program prints clean, readable messages instead of raw JSON blobs.

These aren’t advanced tricks. They’re foundational habits you can apply to every API project. Starting with these patterns now means you won’t have to unlearn brittle, “demo-only” styles of code later.

11. Chapter Summary

In this chapter, you went from making your very first API call to building a small, multi-API program that feels like a real application. Along the way, you met the core ideas that show up in almost every API project: send a request, inspect the response, parse the data, and handle anything that goes wrong.

Key Concepts You Now Know

  • How to make GET requests with requests.get() and talk to real web APIs.
  • What a Response object is and how to read its status code, headers, and body.
  • How JSON maps naturally onto Python dictionaries and lists, and how response.json() does the parsing for you.
  • Why timeout=... is essential on every request so your program never hangs forever.
  • How to use response.raise_for_status() and try/except blocks to handle errors gracefully.
  • How query parameters work, and how the params argument builds safe URLs for you.
  • How to structure code into small, focused functions that each handle a single API’s behavior.

None of these patterns are “toy” techniques. They’re the same building blocks used in production apps that talk to payment providers, weather services, mapping APIs, social media platforms, and more.

The Cat Facts and Jokes program might be playful, but the way you designed it, with timeouts, error handling, and clean JSON parsing, is exactly how serious systems are built.

As you move into the next chapters, you’ll reuse this same pattern over and over again: request → inspect → parse → handle errors → present results. The tools and APIs will change, but the mental model you built in this chapter will stay with you.

12. Chapter Summary

You've just crossed a major threshold. You wrote code that connects to real servers across the internet, receives live data, and parses that data into structures you can work with. You're no longer just running scripts that live entirely on your computer. You're building programs that talk to the outside world.

In this chapter, you:

  • Sent real HTTP requests across the internet and received live responses
  • Parsed JSON into Python dictionaries and lists
  • Added timeouts so your code never hangs forever
  • Handled common errors with try/except and raise_for_status()
  • Customized requests with query parameters and the params argument
  • Built a small project that combined multiple APIs into one script

If some of the concepts still feel a bit new, that's completely normal. The goal isn't to memorize every method or function; it's to feel comfortable with the overall flow of making requests, inspecting responses, and handling the cases where things go wrong. You've built that foundation, and everything from here builds on these patterns.

Key Skills Mastered

1.

Making HTTP Requests with Python

You can now use the requests library to send GET requests to any API endpoint, retrieve live data from servers, and bring that information into your Python programs. You understand that requests.get(url) is the core pattern for fetching data from the web.

2.

Working with Response Objects

You've learned to inspect the response object that requests.get() returns. You can check the status code with response.status_code, read the raw text with response.text, and parse JSON data into Python dictionaries and lists with response.json().

3.

Understanding and Parsing JSON

You can read JSON responses from APIs and navigate their structure just like you would navigate Python dictionaries and lists. You understand that JSON is the universal format for structured data on the web, and you know how to extract the specific pieces of information you need from nested structures.

4.

Handling Errors and Timeouts

You've learned to write defensive code that anticipates network failures. You can use try/except blocks to catch request exceptions, include timeouts to prevent your program from hanging indefinitely, and use raise_for_status() to turn silent HTTP errors into explicit exceptions you can handle.

5.

Customizing Requests with Parameters

You understand how to use query parameters to customize API requests, passing a dictionary to the params argument so that requests builds safe, properly formatted URLs for you. You know this approach is cleaner and more reliable than manually constructing query strings.

6.

Building Multi-API Programs

You built a complete program that integrates data from multiple APIs, formats the output in a user-friendly way, and handles errors for each request independently. This is the pattern you'll use in every real-world project: combine multiple data sources, process the responses, and present the results in a way that makes sense to your users.

Chapter Review Quiz

Test your understanding of the key concepts from this chapter:

Select question to reveal the answer:
What’s the difference between response.text and response.json()?

response.text gives you the raw response body as a string. Python just sees characters; it doesn’t know anything about structure.

response.json() parses that text as JSON and returns a Python object (usually a dictionary or list). Now you can access fields with keys, loop over lists, and treat the data like normal Python structures.

Why should every request include a timeout parameter?

Without a timeout, your program is willing to wait forever if a server never responds. That can freeze a script or lock up an entire application, forcing users to manually kill the process.

With a timeout (e.g., timeout=10), the request fails fast with a clear exception after 10 seconds, giving you a chance to log the error, show a friendly message to the user, or try again later-all without hanging the program. Setting a timeout is a simple way to make your code far more robust and professional.

What does response.raise_for_status() do, and why is it useful?

response.raise_for_status() checks the HTTP status code on the response and raises an HTTPError if it’s a 4xx or 5xx error. This is useful because it turns “silent failures” into explicit exceptions you can catch and handle in a try/except block.

It encourages a healthy pattern: assume a request might fail, let raise_for_status() surface the problem, and then handle it cleanly in your code.

Why is using the params argument better than building query strings by hand?

The params argument takes a dictionary and handles all the low-level details for you:

  • It automatically URL-encodes spaces, special characters, and other symbols that aren't allowed in URLs.
  • It converts Python types (integers, booleans, etc.) to strings in the correct format.
  • It builds the ?key=value&key2=value2 part of the URL safely and correctly.

This avoids subtle bugs caused by manual string formatting (like forgetting to encode spaces or special characters) and makes your code clearer, more maintainable, and less error-prone. Let the library handle the details so you can focus on what the parameters mean, not how they're formatted.

What is JSON, and why do APIs use it for responses?

JSON (JavaScript Object Notation) is a text-based format for representing structured data. It uses a syntax similar to Python dictionaries and lists, with keys, values, nested objects, and arrays. It's human-readable but also easy for programs to parse.

APIs use JSON because it's a universal standard that works across all programming languages. Whether you're writing Python, JavaScript, Java, or any other language, you can parse JSON and work with the data. It's lightweight, easy to read, and has become the de facto format for web APIs. When you call response.json(), Python automatically converts JSON text into native Python objects you can work with immediately.

How do you handle the case where an API request fails due to a network error?

You wrap your request in a try/except block and catch requests.RequestException (or one of its more specific subclasses like Timeout, ConnectionError, or HTTPError).

In the except block, you can log the error, print a user-friendly message, return a default value, or take any other appropriate action. The key insight is that network requests are inherently unreliable-servers go down, networks fail, timeouts occur-so your code needs to anticipate failure and handle it gracefully rather than crashing.

What information does the response object give you beyond just the data?

The response object contains much more than just the body of the response. It includes:

  • response.status_code - The HTTP status code (200, 404, 500, etc.) that indicates whether the request succeeded or failed.
  • response.headers - A dictionary of HTTP headers the server sent back, which can include content type, caching information, rate limit details, and more.
  • response.url - The final URL that was requested (useful if the request was redirected).
  • response.elapsed - How long the request took to complete.
  • response.text and response.json() - The actual data returned by the API.

All of this metadata is useful for debugging, logging, and understanding exactly what happened during the request-response cycle. Professional developers inspect these attributes routinely, especially when troubleshooting issues or building robust applications.

What's the difference between a 4xx and a 5xx HTTP status code?

4xx status codes indicate client errors, meaning the problem is with your request. For example, 404 means the resource wasn't found, 400 means the request was malformed, and 401 means you're not authenticated. These errors suggest you need to fix something about the request you're sending.

5xx status codes indicate server errors, meaning the problem is on the server side. For example, 500 means an internal server error, and 503 means the service is temporarily unavailable. These errors suggest the server itself is having problems, and there may not be anything you can do except retry later or contact the API provider.

Understanding this distinction helps you decide how to handle errors: 4xx errors often need code changes, while 5xx errors often just need retry logic or user notifications.

Looking Forward

You've learned how to make API calls, parse responses, and handle basic errors. But real-world APIs come with more complexity: authentication requirements, rate limits, pagination, and detailed error messages that you need to interpret correctly.

In the next chapter, you'll learn how to read API documentation like a professional developer. Documentation is where APIs explain their rules, endpoints, required parameters, and authentication schemes. Once you can read documentation fluently, you'll be able to integrate any API into your projects, not just the handful we've covered here.

You're building the skillset of a professional API developer, one chapter at a time. Keep going. You're doing great.