Python API requests tutorial thumbnail

Python Requests Library: How to Call APIs in Python (Complete 2026 Guide)

You’ve written your first Python script. Maybe it processes a CSV, automates a task, or crunches some numbers. Now you want it to talk to the internet — pull in live weather data, post a message to Slack, fetch your GitHub repos, or call any one of the thousands of public APIs available today.

That’s where the Requests library comes in.

Requests is a Python library that makes sending HTTP requests — the kind your browser makes every time you visit a website — as simple as calling a function. It’s so clean and readable that its own tagline is “HTTP for Humans.” As of June 2026, it remains the most widely downloaded Python package on PyPI and the first tool most developers reach for when calling an API.

In this guide you’ll learn everything from making your very first GET request to handling authentication, managing sessions, downloading files, and writing production-ready error handling. Every code example in this article is real and runnable.

What you’ll learn:

  • What Requests is and why it’s still the standard in 2026
  • How to install it and make your first API call
  • All HTTP methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
  • Sending headers, query params, and JSON data
  • Authentication with API keys, Bearer tokens, and Basic Auth
  • Error handling, timeouts, retries, and sessions
  • File uploads and downloads
  • Security best practices (including a 2024/2025 CVE you should know about)
  • When Requests is the right tool — and when it isn’t

Let’s get into it.


What Is the Python Requests Library?

The Requests library is a third-party Python package for sending HTTP requests. At its core, it lets your Python code communicate with web servers — the same way a browser does when you visit a URL.

Python does include a built-in module for this (urllib), but it’s famously verbose and awkward to use. Requests wraps all that complexity behind a clean, human-friendly interface. Here’s a comparison:

With urllib (built-in):

import urllib.request
import json

req = urllib.request.Request("https://api.github.com/user")
req.add_header("Authorization", "Bearer mytoken")
with urllib.request.urlopen(req) as response:
    data = json.loads(response.read().decode("utf-8"))
print(data["login"])

With Requests:

import requests

response = requests.get(
    "https://api.github.com/user",
    headers={"Authorization": "Bearer mytoken"}
)
print(response.json()["login"])

Same result. Half the code. That’s the Requests advantage.

Under the hood, Requests is built on top of urllib3, a lower-level library that handles connection pooling, retries, and SSL. You get all of that power without ever having to think about it.

The current stable version is 2.32.5, released March 2026.


Why Developers Still Use Requests in 2026

With newer alternatives like httpx and aiohttp gaining attention, you might wonder if Requests is still worth learning. The answer is a clear yes — for most use cases.

Here’s why it’s still the default choice:

  • Zero learning curve — the API feels like natural English
  • Massive community — Stack Overflow has more Requests answers than any other HTTP library
  • Battle-tested — it’s been in production use since 2011
  • Excellent documentationrequests.readthedocs.io is one of the best in the Python ecosystem
  • Perfect for synchronous scripts — which describes the majority of automation, data collection, and API integration work

Requests vs httpx vs aiohttp — Which Should You Use?

Here’s an honest comparison:

LibrarySyncAsyncHTTP/2Best For
requestsSimple scripts, beginners, single requests
httpxModern apps needing async or HTTP/2
aiohttpHigh-concurrency asyncio applications

Pro Tip: If you’re learning or writing small-to-medium scripts, Requests is perfect. If you’re building an app that fires off dozens of API calls at the same time, you’ll want to look at httpx or aiohttp later. Don’t optimize for problems you don’t have yet.

If you’re curious about the API side of things — building endpoints rather than calling them — How to Build a REST API Using FastAPI is a great companion to this article.


How to Install the Requests Library

Requests is not part of the Python standard library, so you install it with pip.

Requirements: Python 3.9 or higher (Requests 2.32.x dropped support for Python 3.8).

Installing with pip

pip install requests

To pin to the latest stable version:

pip install "requests>=2.32.5"

Verifying the Installation

import requests
print(requests.__version__)
# Should output: 2.32.5

Beginner Tip: If you’re working on multiple Python projects, always use a virtual environment so package versions don’t clash between projects.

python -m venv venv
source venv/bin/activate    # Mac/Linux
venv\Scripts\activate       # Windows
pip install requests

If you run into import errors or version conflicts after installing, How to Debug Python Code Step by Step walks you through diagnosing and fixing them.


HTTP Basics — What Actually Happens When You Call an API

Before writing a single line of Requests code, it helps to understand what’s happening behind the scenes.

When your Python script calls an API, it’s doing the same thing your browser does every time you visit a website:

  1. Your code (the client) sends an HTTP request to a server
  2. The server processes it and sends back an HTTP response
  3. Your code reads the response and does something with it

Think of it like ordering at a restaurant. You (client) place an order (request). The kitchen (server) prepares it and sends it back (response). If something goes wrong — wrong item, kitchen closed — you get an error message (status code).

HTTP Methods

Every request uses a method that tells the server what you want to do:

MethodPurposeIdempotent?
GETRetrieve data
POSTCreate a new resource
PUTReplace a resource entirely
PATCHUpdate part of a resource
DELETERemove a resource
HEADLike GET but no response body
OPTIONSAsk what methods are allowed

HTTP Status Codes

The server always sends back a status code telling you whether the request worked:

  • 2xx — Success (200 OK, 201 Created, 204 No Content)
  • 4xx — Client error, something wrong with your request (400 Bad Request, 401 Unauthorized, 404 Not Found, 429 Too Many Requests)
  • 5xx — Server error, not your fault (500 Internal Server Error, 503 Service Unavailable)

We’ll cover these in detail when we get to error handling.


Making Your First API Request

Let’s write some real code. We’ll use JSONPlaceholder — a free, public API that returns fake but well-structured data. No signup required.

Your First GET Request

import requests

response = requests.get("https://jsonplaceholder.typicode.com/posts/1")
print(response.status_code)  # 200
print(response.json())

Run that and you’ll see something like:

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit..."
}

In two lines, you’ve called a live API and decoded the JSON response into a Python dictionary.

The Response Object

Every Requests call returns a Response object. Here are the most useful attributes:

response = requests.get("https://jsonplaceholder.typicode.com/posts/1")

print(response.status_code)   # 200
print(response.ok)            # True if status < 400
print(response.headers)       # Dict-like object of response headers
print(response.text)          # Response body as a string
print(response.content)       # Response body as raw bytes
print(response.json())        # Decode JSON response → Python dict/list
print(response.url)           # Final URL (after any redirects)
print(response.elapsed)       # How long the request took (timedelta)

Beginner Tip: response.json() is the method you’ll use in 90% of API calls. It converts the JSON payload the server sends into a Python dictionary you can access with normal dict syntax.


HTTP Methods — When and How to Use Each One

GET — Fetching Data

GET is the most common method. Use it to retrieve data without changing anything on the server.

import requests

response = requests.get("https://jsonplaceholder.typicode.com/posts/1")
post = response.json()
print(post["title"])

GET requests are safe (they don’t modify anything) and idempotent (calling them 10 times gives the same result as calling them once).

POST — Creating a New Resource

Use POST when you want to create something new — a user account, a blog post, a comment.

import requests

new_post = {
    "title": "My First Post",
    "body": "This is the content of my post.",
    "userId": 1
}

response = requests.post(
    "https://jsonplaceholder.typicode.com/posts",
    json=new_post
)

print(response.status_code)  # 201 Created
print(response.json())       # Returns the created resource with an id

Common Mistake: Using data=new_post instead of json=new_post sends form-encoded data (application/x-www-form-urlencoded), not JSON. Most modern APIs expect application/json. Always use json= when the API wants JSON.

PUT — Replacing a Resource

PUT replaces an entire resource with what you send. If you omit a field, it disappears.

response = requests.put(
    "https://jsonplaceholder.typicode.com/posts/1",
    json={
        "id": 1,
        "title": "Completely Updated Title",
        "body": "Completely new body text.",
        "userId": 1
    }
)
print(response.json())

PATCH — Updating Part of a Resource

PATCH is more efficient than PUT when you only want to update specific fields.

response = requests.patch(
    "https://jsonplaceholder.typicode.com/posts/1",
    json={"title": "Just the title has changed"}
)
print(response.json())

DELETE — Removing a Resource

response = requests.delete("https://jsonplaceholder.typicode.com/posts/1")
print(response.status_code)  # 200 or 204

A 204 response means “success, no content to return.” This is normal for DELETE.

HEAD and OPTIONS

These are less common but useful in specific scenarios.

HEAD — Returns only the headers, not the body. Useful for checking if a resource exists or getting its content type without downloading the whole thing:

response = requests.head("https://jsonplaceholder.typicode.com/posts/1")
print(response.headers["Content-Type"])
print(response.headers["Content-Length"])
# No body is downloaded

OPTIONS — Returns the HTTP methods supported by an endpoint. Used in CORS preflight checks:

response = requests.options("https://httpbin.org/anything")
print(response.headers.get("Allow"))

Sending Query Parameters

Query parameters are the ?key=value pairs you see in URLs. Instead of building them manually and risking encoding errors, let Requests handle it:

import requests

# Don't do this:
# response = requests.get("https://api.example.com/search?q=python&page=2&limit=10")

# Do this instead:
params = {
    "q": "python",
    "page": 2,
    "limit": 10
}

response = requests.get("https://httpbin.org/get", params=params)
print(response.url)
# https://httpbin.org/get?q=python&page=2&limit=10

Requests URL-encodes the parameters automatically — spaces, special characters, everything. You can verify the final URL with response.url.

URL Path Parameters

For dynamic segments in the URL path (like /users/42), use f-strings:

user_id = 42
endpoint = f"https://jsonplaceholder.typicode.com/users/{user_id}"
response = requests.get(endpoint)
print(response.json()["name"])

Security Note: If user_id comes from user input, validate and sanitize it before embedding it in a URL. Never trust raw user input directly.


Working with Headers

Sending Custom Headers

Many APIs require specific headers — a custom User-Agent, an Accept type, or a versioning header.

import requests

headers = {
    "User-Agent": "MyPythonApp/1.0",
    "Accept": "application/json",
    "X-API-Version": "2"
}

response = requests.get("https://httpbin.org/headers", headers=headers)
print(response.json())

Reading Response Headers

response = requests.get("https://jsonplaceholder.typicode.com/posts/1")

print(response.headers["Content-Type"])
# application/json; charset=utf-8

remaining = response.headers.get("X-RateLimit-Remaining")
if remaining:
    print(f"API calls remaining: {remaining}")

Quick Note: Header keys in Requests are case-insensitive. response.headers["content-type"] and response.headers["Content-Type"] return the same value. Use .get() for optional headers so you don’t get a KeyError if they’re absent.


Authentication — Securing Your API Calls

Most production APIs require authentication. Here are the patterns you’ll encounter most.

API Key in a Header

This is the most common approach for public APIs:

import requests
import os

api_key = os.environ.get("MY_API_KEY")  # Never hardcode this

headers = {"X-Api-Key": api_key}
response = requests.get("https://api.example.com/data", headers=headers, timeout=10)

Security Warning: Never hardcode API keys directly in your scripts. If you push to GitHub, bots scrape public repos for keys within seconds. Always use environment variables or a .env file with python-dotenv:

pip install python-dotenv
from dotenv import load_dotenv
import os

load_dotenv()  # Loads .env file
api_key = os.environ.get("MY_API_KEY")

Bearer Token (OAuth 2.0)

The standard for modern APIs — you get a token after logging in or via OAuth, then pass it with every request:

import requests, os

token = os.environ.get("API_TOKEN")
headers = {"Authorization": f"Bearer {token}"}

response = requests.get("https://api.example.com/me", headers=headers, timeout=10)
response.raise_for_status()
print(response.json())

HTTP Basic Authentication

Some older APIs (and internal tools) still use Basic Auth. Requests handles it cleanly:

from requests.auth import HTTPBasicAuth
import requests

# Long form
response = requests.get(
    "https://httpbin.org/basic-auth/user/pass",
    auth=HTTPBasicAuth("user", "pass"),
    timeout=10
)

# Shorthand — identical result
response = requests.get(
    "https://httpbin.org/basic-auth/user/pass",
    auth=("user", "pass"),
    timeout=10
)

print(response.status_code)  # 200

HTTP Digest Authentication

Less common, but Requests supports it out of the box:

from requests.auth import HTTPDigestAuth

response = requests.get(
    "https://httpbin.org/digest-auth/auth/user/pass",
    auth=HTTPDigestAuth("user", "pass"),
    timeout=10
)

For an example of how authentication works in a real web application, How to Create a Login System in Flask covers many of the same concepts from the server side.


Sending and Receiving JSON

Sending JSON Data

import requests

payload = {
    "name": "Alice",
    "email": "alice@example.com",
    "role": "developer"
}

response = requests.post(
    "https://jsonplaceholder.typicode.com/users",
    json=payload,    # Automatically sets Content-Type: application/json
    timeout=10
)

print(response.status_code)
print(response.json())

Using json= automatically:

  1. Serializes your Python dictionary to a JSON string
  2. Sets the Content-Type: application/json header

You don’t need to call json.dumps() yourself.

Parsing JSON Responses

response = requests.get("https://jsonplaceholder.typicode.com/users/1", timeout=10)
user = response.json()

# Access fields like a normal dict
print(user["name"])
print(user["email"])

# Use .get() for optional fields (safer — won't raise KeyError)
phone = user.get("phone", "No phone provided")
print(phone)

Handling Non-JSON Responses

Not every API returns JSON. Some return XML, CSV, or plain text. Always check before calling .json():

response = requests.get("https://api.example.com/data", timeout=10)

if "application/json" in response.headers.get("Content-Type", ""):
    data = response.json()
else:
    # Handle as text
    print(response.text)

If you call .json() on a response that isn’t valid JSON, you’ll get a requests.exceptions.JSONDecodeError. Catching that is covered in the error handling section below.


Handling HTTP Status Codes

Every response from an API includes a status code telling you what happened. Knowing the most common ones saves a lot of debugging time.

Status Code Reference

CodeNameTypical Cause
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST
204No ContentSuccessful DELETE
400Bad RequestMalformed request body or params
401UnauthorizedMissing or invalid auth credentials
403ForbiddenValid credentials, but no permission
404Not FoundEndpoint or resource doesn’t exist
429Too Many RequestsRate limit exceeded
500Internal Server ErrorBug on the server’s side
503Service UnavailableServer overloaded or down

Checking Status Codes

import requests

response = requests.get("https://jsonplaceholder.typicode.com/posts/1", timeout=10)

if response.status_code == 200:
    data = response.json()
    print(data["title"])
elif response.status_code == 404:
    print("That resource doesn't exist.")
elif response.status_code == 401:
    print("Check your API key — authentication failed.")
elif response.status_code == 429:
    retry_after = response.headers.get("Retry-After", "unknown")
    print(f"Rate limited. Wait {retry_after} seconds before retrying.")

Using raise_for_status() — The Recommended Approach

Manually checking every status code is tedious. raise_for_status() throws an HTTPError for any 4xx or 5xx response, and does nothing for 2xx:

import requests

response = requests.get("https://jsonplaceholder.typicode.com/posts/1", timeout=10)
response.raise_for_status()  # Raises HTTPError if status is 4xx or 5xx
data = response.json()
print(data["title"])

Combine it with a try/except block for clean error handling (shown in the next section).

Pro Tip: Always call raise_for_status() right after your request, before doing anything with the response. It protects you from silently processing error responses as if they were successful ones.


Error Handling and Exceptions

Production code never assumes the API will work perfectly. Networks drop, servers go down, tokens expire. Handle it gracefully.

The Complete Error Handling Pattern

import requests

def fetch_post(post_id: int) -> dict | None:
    url = f"https://jsonplaceholder.typicode.com/posts/{post_id}"
    
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        return response.json()

    except requests.exceptions.ConnectionError:
        print("Connection failed — check the URL or your network.")
    except requests.exceptions.Timeout:
        print("Request timed out — the server took too long to respond.")
    except requests.exceptions.HTTPError as e:
        print(f"HTTP error {e.response.status_code}: {e}")
    except requests.exceptions.JSONDecodeError:
        print("Response wasn't valid JSON.")
    except requests.exceptions.RequestException as e:
        # Catches any other requests-related error
        print(f"Something went wrong: {e}")
    
    return None

post = fetch_post(1)
if post:
    print(post["title"])

Exception Hierarchy

Understanding which exception catches what prevents silent bugs:

requests.exceptions.RequestException   ← Catch-all base class
├── ConnectionError
│   ├── ProxyError
│   └── SSLError
├── HTTPError                          ← Raised by raise_for_status()
├── URLRequired
├── TooManyRedirects
├── Timeout
│   ├── ConnectTimeout
│   └── ReadTimeout
└── JSONDecodeError                    ← When .json() fails

Beginner Tip: Always catch the most specific exception first, then fall back to RequestException as a safety net. This gives you more useful error messages for the errors you actually expect.


Timeouts — Never Let a Request Hang Forever

This is one of the most important things beginners get wrong.

By default, Requests has no timeout. If the server never responds, your script hangs forever. Always set one.

Setting a Timeout

import requests

# Single value — applies to both connect and read phases
response = requests.get("https://api.example.com/data", timeout=10)

# Tuple — (connect timeout, read timeout)
# Connect: how long to wait to establish the connection
# Read: how long to wait for the server to send data
response = requests.get("https://api.example.com/data", timeout=(5, 30))

Choosing a Timeout Value

ScenarioRecommended Timeout
Fast internal APIstimeout=5
Third-party APIstimeout=10
Large data responsestimeout=(5, 60)
File uploadstimeout=(5, 120)

These are starting points — adjust based on what you observe in practice.

Warning: A ConnectTimeout means your code couldn’t reach the server at all. A ReadTimeout means the server connected but didn’t finish sending data in time. The tuple form lets you handle these differently.


Sessions — Reusing Connections for Speed and Convenience

A Session is the most underused feature in Requests. It does two things:

  1. Persists settings — headers, auth, cookies — across multiple requests
  2. Reuses TCP connections — connection pooling means you don’t pay the TCP handshake cost on every call

When you make multiple requests to the same server (very common), Sessions can reduce response times by 30–50%.

Basic Session Usage

import requests

with requests.Session() as session:
    # These headers apply to every request through this session
    session.headers.update({
        "Authorization": "Bearer my_api_token",
        "Accept": "application/json"
    })
    
    # No need to pass headers to each call
    user = session.get("https://api.example.com/user", timeout=10)
    posts = session.get("https://api.example.com/user/posts", timeout=10)
    
    user.raise_for_status()
    posts.raise_for_status()
    
    print(user.json()["name"])
    print(len(posts.json()), "posts")

The with block ensures the session is closed properly when you’re done, releasing the network connection back to the pool.

Session-Level vs Request-Level Settings

You can set defaults on the Session and override them per-request:

with requests.Session() as session:
    # Applies to all requests
    session.auth = ("username", "password")
    session.headers.update({"X-App-Version": "2.0"})
    session.verify = True  # Always verify SSL (this is the default — keep it)

    # Override just for this one request
    slow_endpoint = session.get(
        "https://api.example.com/slow-report",
        timeout=(5, 120)  # Longer read timeout for this specific call
    )

A Critical Security Note (CVE-2024-35195)

A vulnerability was discovered and fixed in Requests 2.32.0: if you called any request with verify=False through a Session, all subsequent requests to the same host would silently ignore SSL verification — even if you later set verify=True. This persisted for the lifetime of the connection in the pool.

The fix: Always use Requests 2.32.2 or higher, and never use verify=False in production. If you temporarily need to skip verification in development, create a fresh Session object rather than reusing one that previously had verify=False.

# DON'T reuse a session that had verify=False
# DO create a fresh one when your verification requirements change

with requests.Session() as secure_session:
    secure_session.verify = True  # Explicit and clear
    response = secure_session.get("https://api.example.com/data", timeout=10)

Cookies

Sending Cookies

import requests

cookies = {"session_id": "abc123", "theme": "dark"}
response = requests.get(
    "https://httpbin.org/cookies",
    cookies=cookies,
    timeout=10
)
print(response.json())

Reading Cookies from a Response

response = requests.get("https://httpbin.org/cookies/set/mykey/myvalue", timeout=10)
print(response.cookies.get("mykey"))

Persistent Cookies with Sessions

Sessions automatically store cookies from responses and send them back on the next request to the same domain — exactly what a browser does:

with requests.Session() as session:
    # Login — server sets a session cookie
    session.post("https://api.example.com/login", json={"user": "alice", "pass": "secret"}, timeout=10)
    
    # The session cookie is sent automatically on subsequent requests
    profile = session.get("https://api.example.com/profile", timeout=10)
    print(profile.json())

File Uploads

Uploading a Single File

import requests

with open("document.pdf", "rb") as file:
    files = {"file": file}
    response = requests.post(
        "https://httpbin.org/post",
        files=files,
        timeout=60
    )

print(response.status_code)

Uploading with a Custom Filename and MIME Type

with open("photo.jpg", "rb") as file:
    files = {"photo": ("custom_name.jpg", file, "image/jpeg")}
    data = {"caption": "My photo", "album_id": "123"}
    
    response = requests.post(
        "https://api.example.com/photos",
        files=files,
        data=data,
        timeout=60
    )

Common Mistake: Don’t combine files= with json=. They use different content types (multipart/form-data vs application/json) and can’t be mixed. Use data= alongside files= for additional form fields.


Downloading Files

Downloading a Small File

import requests

response = requests.get("https://example.com/report.pdf", timeout=30)
response.raise_for_status()

with open("report.pdf", "wb") as file:
    file.write(response.content)

print("Downloaded successfully.")

Downloading Large Files with Streaming

For large files, stream=True tells Requests not to load the entire response into memory. Instead, you read it in chunks:

import requests

url = "https://example.com/large-dataset.csv"

with requests.get(url, stream=True, timeout=60) as response:
    response.raise_for_status()
    
    with open("dataset.csv", "wb") as file:
        for chunk in response.iter_content(chunk_size=8192):  # 8KB chunks
            file.write(chunk)

print("Large file downloaded successfully.")

Pro Tip: Without stream=True, Requests downloads the entire file into memory before you can write it to disk. For a 500MB file, that’s 500MB of RAM used at once. With stream=True, you use roughly 8KB at a time regardless of file size.

Once you’ve downloaded a file, you’ll often want to read and process it. How to Read and Write Text Files in Python covers everything you need to work with the content.


Real-World Examples

Let’s put it all together with examples that use real, public APIs.

Example 1 — The GitHub API

import requests
import os

def get_github_repos(username: str) -> list:
    """Fetch all public repos for a GitHub user."""
    
    token = os.environ.get("GITHUB_TOKEN")  # Optional — increases rate limit
    
    headers = {"Accept": "application/vnd.github+json"}
    if token:
        headers["Authorization"] = f"Bearer {token}"
    
    with requests.Session() as session:
        session.headers.update(headers)
        
        response = session.get(
            f"https://api.github.com/users/{username}/repos",
            params={"per_page": 30, "sort": "updated"},
            timeout=10
        )
        response.raise_for_status()
        
        repos = response.json()
        return [{"name": r["name"], "stars": r["stargazers_count"]} for r in repos]

repos = get_github_repos("psf")
for repo in repos[:5]:
    print(f"{repo['name']}: ⭐ {repo['stars']}")

Example 2 — A Weather API (OpenWeatherMap)

import requests
import os

def get_weather(city: str) -> dict:
    """Get current weather for a city."""
    
    api_key = os.environ.get("OPENWEATHER_KEY")
    
    try:
        response = requests.get(
            "https://api.openweathermap.org/data/2.5/weather",
            params={
                "q": city,
                "appid": api_key,
                "units": "metric"
            },
            timeout=10
        )
        response.raise_for_status()
        data = response.json()
        
        return {
            "city": data["name"],
            "temperature": data["main"]["temp"],
            "description": data["weather"][0]["description"],
            "humidity": data["main"]["humidity"]
        }
    
    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 404:
            print(f"City '{city}' not found.")
        else:
            print(f"API error: {e}")
        return {}
    except requests.exceptions.RequestException as e:
        print(f"Request failed: {e}")
        return {}

weather = get_weather("London")
if weather:
    print(f"{weather['city']}: {weather['temperature']}°C, {weather['description']}")

Once you’ve pulled API data into Python, a common next step is analyzing it. Pandas Tutorial for Beginners shows you how to work with that JSON data in a DataFrame.

You can also send HTTP API calls to trigger automations — for example, posting a message to a messaging platform. If that sounds useful, How to Automate WhatsApp Messages Using Python is a practical walkthrough of that exact use case.


Advanced Patterns

Retries with Exponential Backoff

APIs fail. Rate limits get hit. Servers hiccup. A good API client retries sensibly instead of crashing or hammering the server:

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def create_session_with_retries() -> requests.Session:
    """Return a Session that retries on common transient errors."""
    
    retry_strategy = Retry(
        total=3,               # Maximum 3 retry attempts
        backoff_factor=1,      # Wait 1s, 2s, 4s between retries
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["GET", "POST"]
    )
    
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session = requests.Session()
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    
    return session

session = create_session_with_retries()
response = session.get("https://api.example.com/data", timeout=10)
response.raise_for_status()

Pro Tip: Note that 429 Too Many Requests is in the status_forcelist. If you hit a rate limit, the retry strategy will back off automatically before trying again. Always check the Retry-After response header too — it tells you exactly how long to wait.

Proxy Support

import requests

proxies = {
    "http": "http://proxy.example.com:8080",
    "https": "http://proxy.example.com:8080"
}

response = requests.get(
    "https://api.example.com/data",
    proxies=proxies,
    timeout=10
)

Streaming Responses for Large or Real-Time Data

import requests

def stream_large_response(url: str):
    with requests.get(url, stream=True, timeout=60) as response:
        response.raise_for_status()
        for line in response.iter_lines():
            if line:
                decoded = line.decode("utf-8")
                process(decoded)  # Replace with your processing logic

Performance Tips

1. Always Use Sessions for Multiple Requests

This is the single biggest performance improvement most scripts can make. TCP connection reuse eliminates the handshake overhead on every call.

# Slow — new TCP connection every time
for item_id in range(100):
    requests.get(f"https://api.example.com/items/{item_id}")

# Fast — one connection, reused 100 times
with requests.Session() as session:
    for item_id in range(100):
        session.get(f"https://api.example.com/items/{item_id}", timeout=10)

2. Set Sensible Timeouts Everywhere

No timeout = program that can hang indefinitely. Always pass timeout=.

3. Use stream=True for Large Responses

Keeps memory usage constant regardless of file size.

4. When Requests Hits Its Limit

Requests is synchronous — each request waits for the previous one to finish. For 10 API calls, that’s 10 round trips, one after the other.

If you need to make many requests concurrently, consider httpx with Python’s async/await. The migration is minimal because httpx has a nearly identical API:

# requests
response = requests.get(url)

# httpx (sync — identical)
import httpx
response = httpx.get(url)

# httpx (async — parallel requests)
async with httpx.AsyncClient() as client:
    response = await client.get(url)

For a deeper look at running code concurrently in Python, Multithreading in Python: Beginner Guide covers threading patterns you can combine with Requests today.


Security Best Practices

Always Verify SSL Certificates

# Correct — SSL verification is on by default
response = requests.get("https://api.example.com", verify=True, timeout=10)

# NEVER do this in production
response = requests.get("https://api.example.com", verify=False)  # ❌ Dangerous

verify=False disables SSL certificate validation entirely. This exposes your connection to man-in-the-middle attacks where an attacker can intercept and modify the data in transit. If you see an SSLError, fix the underlying certificate problem — don’t silence the error.

Protect Your Credentials

# ❌ Never do this
api_key = "sk-abc123mysecretkey"

# ✅ Do this
import os
api_key = os.environ.get("MY_API_KEY")
if not api_key:
    raise ValueError("MY_API_KEY environment variable not set")

Validate URLs

Never pass raw user input directly as a URL:

from urllib.parse import urlparse

def is_safe_url(url: str) -> bool:
    parsed = urlparse(url)
    return parsed.scheme in ("http", "https") and bool(parsed.netloc)

user_url = input("Enter URL: ")
if is_safe_url(user_url):
    response = requests.get(user_url, timeout=10)

Keep Requests Updated

  • Requests 2.32.0: Fixed CVE-2024-35195 (SSL verification bypass in Sessions)
  • Requests 2.32.4: Fixed a URL parsing issue that could leak .netrc credentials to wrong hosts
  • Requests 2.32.5 (March 2026): Current stable release

Pin your version:

# requirements.txt
requests>=2.32.5

Run pip audit in CI to catch new CVEs automatically.

Security Checklist

  • [ ] verify=True always (never disable in production)
  • [ ] API keys loaded from environment variables
  • [ ] timeout= set on every request
  • [ ] requests>=2.32.5 pinned in requirements.txt
  • [ ] pip audit running in CI/CD pipeline
  • [ ] verify=False grep blocked in code review

Common Mistakes and How to Avoid Them

MistakeProblemFix
No timeoutScript hangs foreverAlways pass timeout=
Using data= for JSONSends wrong content-typeUse json= for JSON APIs
Skipping raise_for_status()Silently processes error responsesCall it after every request
Hardcoded credentialsKeys leak via version controlUse environment variables
verify=False in productionMan-in-the-middle attack riskNever. Fix the certificate instead
New requests.get() in a loopReconnects on every iterationUse Session with connection pooling
Ignoring 429 responsesGets IP banned by the APICheck Retry-After, back off, or use retry strategy
Not handling JSONDecodeErrorCrashes when API returns an error pageCheck Content-Type first, or wrap in try/except
Opening files without withFile handle leaksAlways use with open(...)

Troubleshooting Common Issues

ConnectionError

requests.exceptions.ConnectionError: HTTPSConnectionPool...

Causes: Typo in the URL, no internet connection, DNS failure, firewall blocking the request.

Fix: Double-check the URL. Try curl https://api.example.com in your terminal to rule out a network issue. Make sure you included https://.

SSLError

requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED]

Cause: The server’s SSL certificate is invalid, expired, or self-signed.

Fix: Don’t set verify=False. Investigate the certificate. For internal servers with self-signed certs, point verify= to your CA bundle: verify="/path/to/ca-bundle.crt".

Timeout

requests.exceptions.ReadTimeout

Fix: Increase the timeout. Check if the server is responding (try in a browser or with curl). For slow endpoints, use a longer read timeout: timeout=(5, 60).

JSONDecodeError

requests.exceptions.JSONDecodeError: Expecting value: line 1...

Cause: The API returned HTML (usually an error page) instead of JSON.

Fix: Print response.text to see what actually came back. Then fix the underlying problem — usually an auth issue (401/403) or a wrong URL (404).

401 Unauthorized

Your API key is missing, wrong, or expired. Double-check the header name — some APIs use X-Api-Key, others use Authorization. Check the API’s documentation.

429 Too Many Requests

You’ve exceeded the rate limit. Check the Retry-After header:

if response.status_code == 429:
    wait = int(response.headers.get("Retry-After", 60))
    print(f"Rate limited. Waiting {wait} seconds...")
    import time
    time.sleep(wait)

If you frequently hit Python errors you don’t recognize, How to Debug Python Code Step by Step has a systematic approach to tracking them down.


Real-World Use Cases

The Requests library powers a wide variety of real projects:

  • Data collection pipelines — fetch product prices, news headlines, or stock data on a schedule and store them in a database
  • CI/CD automation — trigger deployments, post build statuses to Slack, open GitHub issues automatically
  • Daily task automation — combine API calls with file management and scheduling to automate repetitive work; How to Automate Daily Tasks on Your Computer Using Python covers this in depth
  • Messaging bots — send notifications via email, SMS, WhatsApp, or Telegram through their HTTP APIs; see How to Send Emails Automatically Using Python for a practical example
  • Data science — pull JSON from a REST API, convert it to a Pandas DataFrame, and analyze it — a workflow Pandas Tutorial for Beginners covers end to end

Frequently Asked Questions

What is the Python Requests library used for?

The Requests library lets Python code communicate with web servers and APIs by sending HTTP requests. You can use it to retrieve data with GET, send data with POST, update resources with PUT or PATCH, delete them with DELETE, and much more. It’s the standard tool for API integrations, web automation, data collection, and any Python script that needs to talk to the internet.

Is the Requests library built into Python?

No. Requests is a third-party package and must be installed separately:

pip install requests

Python does include a built-in urllib module, but it’s significantly more verbose. Requests is almost universally preferred.

What is the difference between data= and json= in Requests?

  • Use json= when the API expects application/json (the majority of modern APIs). Requests automatically serializes your dict and sets the correct Content-Type header.
  • Use data= for HTML form submissions (application/x-www-form-urlencoded) or alongside files= for multipart uploads.

When in doubt, check the API documentation for which Content-Type it expects.

How do I add authentication to a Python Requests call?

The most common methods:

# API key in header
headers = {"Authorization": "Api-Key YOUR_KEY"}
requests.get(url, headers=headers)

# Bearer token (OAuth 2.0)
headers = {"Authorization": "Bearer YOUR_TOKEN"}
requests.get(url, headers=headers)

# Basic auth
requests.get(url, auth=("username", "password"))

Always load credentials from environment variables, never hardcode them.

Should I use Requests or httpx in 2026?

Use Requests if:

  • You’re learning Python API calls for the first time
  • Your script makes a small number of sequential requests
  • Simplicity matters more than performance

Use httpx if:

  • You need async/await support for concurrent requests
  • You need HTTP/2 support
  • You’re building a modern web application or microservice

The good news: httpx has an almost identical API to Requests, so switching later is easy.

What happens if I don’t set a timeout?

Your request will wait forever for a server that never responds, hanging your script indefinitely. Always pass timeout=:

requests.get(url, timeout=10)       # 10 seconds for both connect and read
requests.get(url, timeout=(5, 30))  # 5s connect, 30s read

Is verify=False safe to use?

No. Setting verify=False disables SSL certificate validation, leaving your connection open to man-in-the-middle attacks. It should never appear in production code. If you’re seeing SSLError, the correct fix is to resolve the certificate issue, not to silence the validation.

What is a Requests Session and why should I use it?

A Session object reuses the underlying TCP connection across multiple requests to the same server, which reduces overhead. It also persists headers, cookies, and authentication settings automatically:

with requests.Session() as session:
    session.headers.update({"Authorization": "Bearer token"})
    r1 = session.get(url1, timeout=10)
    r2 = session.get(url2, timeout=10)  # Same connection, same auth

Use Sessions any time you’re making more than one request to the same server.


Summary

Here’s what you now know how to do with the Python Requests library:

TaskHow
Make a GET requestrequests.get(url, timeout=10)
Send JSON datarequests.post(url, json=payload, timeout=10)
Add headersPass headers={"Key": "Value"}
Add query paramsPass params={"key": "value"}
Authenticateheaders={"Authorization": "Bearer token"} or auth=("user", "pass")
Handle errorsresponse.raise_for_status() + try/except
Set a timeouttimeout=10 or timeout=(connect, read)
Reuse connectionswith requests.Session() as session:
Download large filesrequests.get(url, stream=True) + iter_content()
Upload filesrequests.post(url, files={"file": open(...)})
Retry on failureHTTPAdapter(max_retries=Retry(...))

The library’s strength is its clarity. You can read a Requests call and immediately understand what it does — which is exactly what makes it “HTTP for Humans.”


Next Steps

Here are a few things to try now that you have the fundamentals:

  1. Call the GitHub API — it’s free, requires no signup for basic use, and has excellent documentation. Try fetching your own public repos.
  2. Build a small project — pick a public API that interests you (weather, movies, space data, crypto prices) and write a script that fetches and displays something useful.
  3. Read the official docsrequests.readthedocs.io covers advanced topics like custom authentication classes, event hooks, and transport adapters.
  4. Learn to build your own API — now that you know how to call APIs, Build a Simple Website Using Flask will show you how to build one.
  5. Prepare for interviews — HTTP, APIs, and Requests are common Python interview topics. Top 20 Python Interview Questions for Beginners will help you prepare.

External Resources

  • Requests Official Documentation — The authoritative reference for all Requests features, from quickstart to advanced usage. Always check here first.
  • Requests on PyPI — The official package page. Check here to confirm the current stable version and view the release history.
  • urllib3 Documentation — Requests is built on top of urllib3. Understanding it helps when you need low-level connection control or are debugging SSL issues.
  • JSONPlaceholder — A free, hosted REST API for testing. No signup, no API key. Perfect for practicing every example in this guide.
  • httpbin.org — Another excellent testing API. Returns exactly what you send it, making it ideal for testing headers, auth, and request bodies.
  • MDN HTTP Status Codes Reference — The definitive reference for every HTTP status code, what it means, and when servers should use it.
  • Python Dotenv on PyPI — The standard way to load API keys and secrets from a .env file into os.environ. Essential for keeping credentials out of your source code.
  • Requests GitHub Repository — View open issues, security advisories, and the changelog. This is where CVEs like CVE-2024-35195 are first disclosed.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *