← Back to DevBytes

How to Build a REST Client with Python Requests Library

Understanding REST Clients and the Python Requests Library

A REST client is a piece of software designed to communicate with RESTful APIs over HTTP. It constructs and sends HTTP requests, then processes the responses—typically JSON or XML—returned by a remote server. In the Python ecosystem, the Requests library is the gold standard for building such clients. It abstracts away the complexities of raw HTTP, cookies, redirects, authentication, and connection pooling behind a clean, human-friendly interface.

What is a REST Client?

A REST (Representational State Transfer) client lets your application interact with web services by sending standard HTTP requests (GET, POST, PUT, DELETE, etc.) to specific URLs and consuming the responses. Whether you're fetching weather data, authenticating users, triggering CI/CD pipelines, or orchestrating cloud infrastructure, you’re relying on a REST client to bridge your code and the remote API.

Why the Requests Library Matters

Python’s standard library includes urllib and http.client, but their interfaces are low-level and cumbersome. Requests offers:

Its popularity means robust documentation, a huge community, and battle-tested reliability, making it the first choice for any Python developer interacting with HTTP APIs.

Getting Started with Requests

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Installation

Requests is available on PyPI and can be installed with a single command. It's recommended to work inside a virtual environment.

python -m venv venv
source venv/bin/activate   # On Windows: venv\Scripts\activate
pip install requests

Making Your First GET Request

The most basic operation is fetching data from an endpoint. Here we query the free JSONPlaceholder API:

import requests

response = requests.get('https://jsonplaceholder.typicode.com/posts/1')
print(response.status_code)
print(response.text)

This snippet issues a GET to retrieve a single post. The status_code (200) indicates success, and .text returns the raw string body.

Understanding the Response Object

The response object contains everything the server sends back. Key properties and methods include:

# HTTP status code as an integer
print(response.status_code)   # 200

# Response body as raw bytes or string
print(response.text)

# Parse JSON response into Python dict/list
data = response.json()
print(data['title'])

# Response headers as a case-insensitive dict
print(response.headers['Content-Type'])

# Check if the request was successful (status < 400)
print(response.ok)            # True

# Raise an exception for HTTP errors (4xx or 5xx)
response.raise_for_status()

Working with HTTP Methods

GET Requests with Query Parameters

Pass query strings cleanly using the params argument. Requests automatically encodes them and appends them to the URL.

params = {
    'userId': 1,
    '_limit': 3
}
response = requests.get('https://jsonplaceholder.typicode.com/posts', params=params)
print(response.url)  # https://jsonplaceholder.typicode.com/posts?userId=1&_limit=3
for post in response.json():
    print(post['id'], post['title'])

POST Requests and Sending JSON

To create resources, use requests.post(). You can send form-encoded data with the data parameter or JSON with the json parameter. Using json automatically sets the Content-Type header to application/json and serializes the payload.

payload = {
    'title': 'foo',
    'body': 'bar',
    'userId': 1
}
response = requests.post('https://jsonplaceholder.typicode.com/posts', json=payload)
print(response.status_code)   # 201 Created
print(response.json()['id'])  # 101

Other HTTP Methods: PUT, DELETE, and PATCH

Updating and deleting resources follows the same pattern. Use the appropriate method and pass the payload if needed.

# PUT (full replacement)
response = requests.put('https://jsonplaceholder.typicode.com/posts/1', json={
    'id': 1,
    'title': 'updated title',
    'body': 'updated body',
    'userId': 1
})
print(response.status_code)  # 200

# PATCH (partial update)
response = requests.patch('https://jsonplaceholder.typicode.com/posts/1', json={
    'title': 'patched title'
})
print(response.json()['title'])

# DELETE
response = requests.delete('https://jsonplaceholder.typicode.com/posts/1')
print(response.status_code)  # 200

Advanced Features for Robust Clients

Custom Headers

Pass a dictionary to the headers parameter. This is essential for API keys, content negotiation, or tracking custom metadata.

headers = {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
    'Accept': 'application/json',
    'User-Agent': 'MyApp/1.0'
}
response = requests.get('https://api.example.com/data', headers=headers)

Authentication

Requests simplifies authentication with dedicated helpers. For Basic Auth, use the auth parameter:

from requests.auth import HTTPBasicAuth

response = requests.get(
    'https://api.example.com/private',
    auth=HTTPBasicAuth('username', 'password')
)
# Alternatively, use a tuple shorthand: auth=('username', 'password')

For Bearer tokens (OAuth2), simply set the Authorization header as shown above. For more complex flows like OAuth1, use the requests-oauthlib package.

Session Objects for Efficiency

A Session reuses the underlying TCP connection (connection pooling) and persists cookies and headers across requests. This dramatically speeds up multiple calls to the same host and reduces resource usage.

session = requests.Session()
session.headers.update({'Accept': 'application/json'})

# All requests through this session share the connection pool
response1 = session.get('https://api.example.com/users')
response2 = session.get('https://api.example.com/posts')

# Session cookies are automatically handled
print(session.cookies.get_dict())

# Don't forget to close the session when done (or use a context manager)
session.close()

Timeouts and Retries

Never let a request hang indefinitely. Set the timeout parameter to limit both connection and read times. Combine with a retry adapter for transient failures.

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

# Create a session with a custom retry strategy
session = requests.Session()
retries = Retry(
    total=3,
    backoff_factor=0.5,
    status_forcelist=[500, 502, 503, 504],
    allowed_methods=['GET', 'POST']
)
adapter = HTTPAdapter(max_retries=retries)
session.mount('https://', adapter)

# All requests via this session will automatically retry
response = session.get('https://api.example.com/unstable', timeout=(2, 5))

The timeout tuple specifies (connect_timeout, read_timeout) in seconds. Always set a timeout to prevent indefinite hangs.

Handling Redirects

By default, Requests follows HTTP redirects (status 301, 302, 303, 307, 308). You can disable this or inspect the redirect chain.

# Disable following redirects
response = requests.get('http://example.com/redirect', allow_redirects=False)
print(response.status_code)  # 301/302

# Access redirect history
response = requests.get('http://example.com/landing-page')
print(response.url)          # final URL
for r in response.history:
    print(r.status_code, r.url)  # intermediate redirects

Streaming Large Responses

For downloading large files or consuming streaming APIs, use stream=True and iterate over response chunks to avoid loading everything into memory.

response = requests.get('https://example.com/large-file.zip', stream=True)
with open('large-file.zip', 'wb') as f:
    for chunk in response.iter_content(chunk_size=8192):
        if chunk:
            f.write(chunk)

File Uploads

POST multipart form data using the files parameter. Requests automatically sets the Content-Type to multipart/form-data.

files = {
    'file': ('report.pdf', open('report.pdf', 'rb'), 'application/pdf'),
    'metadata': (None, '{"description": "monthly report"}', 'application/json')
}
response = requests.post('https://httpbin.org/post', files=files)
print(response.json()['files'])

SSL Certificate Verification

Requests verifies SSL certificates by default. You can provide a custom CA bundle or disable verification (only for development).

# Use a custom CA bundle
response = requests.get('https://self-signed.badssl.com', verify='/path/to/ca-bundle.crt')

# Disable verification (DO NOT USE IN PRODUCTION)
response = requests.get('https://example.com', verify=False)

Error Handling and Resilience

Handling HTTP Errors

Use response.raise_for_status() to turn bad HTTP responses (4xx, 5xx) into exceptions. You can catch them explicitly.

from requests.exceptions import HTTPError

try:
    response = requests.get('https://api.example.com/private')
    response.raise_for_status()
except HTTPError as http_err:
    print(f'HTTP error occurred: {http_err}')
except Exception as err:
    print(f'Other error: {err}')

Connection Errors and Timeouts

Network issues, DNS failures, or timeouts raise specific exceptions. Always catch at least ConnectionError and Timeout.

from requests.exceptions import ConnectionError, Timeout

try:
    response = requests.get('https://api.example.com', timeout=3)
    response.raise_for_status()
except Timeout:
    print('The request timed out')
except ConnectionError:
    print('Failed to connect to the server')
except HTTPError as e:
    print(f'Server returned error: {e.response.status_code}')

Structuring a Robust Request Function

Combine all the above patterns into a reusable function that handles logging, retries, and graceful degradation.

import logging
import requests
from requests.exceptions import RequestException, HTTPError, ConnectionError, Timeout

logger = logging.getLogger(__name__)

def safe_request(method, url, **kwargs):
    try:
        response = requests.request(method, url, timeout=kwargs.pop('timeout', (5, 15)), **kwargs)
        response.raise_for_status()
        return response
    except HTTPError as e:
        logger.error(f'HTTP error {e.response.status_code} for {url}')
        raise
    except Timeout:
        logger.error(f'Timeout while accessing {url}')
        raise
    except ConnectionError:
        logger.error(f'Connection failed for {url}')
        raise
    except RequestException as e:
        logger.error(f'Unexpected error for {url}: {str(e)}')
        raise

Best Practices for a Production-Ready REST Client

Conclusion

Building a REST client with the Python Requests library is a skill that pays off across nearly every domain of software development. You’ve learned how to issue HTTP requests, handle responses, work with authentication, leverage sessions for efficiency, stream large payloads, and implement robust error handling. By following the best practices outlined—always setting timeouts, using sessions, handling exceptions gracefully, and keeping secrets out of your source code—you’ll craft a client that is both reliable and maintainable. With these foundations, you're ready to integrate any RESTful API into your Python applications with confidence.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles