Languages

Creating a Simple API Client with Python: A Step-by-Step Guide

JGJaya Gupta21 Mar 2023 Β· Updated 04 Oct 2026 Β· 8 min read
Creating a Simple API Client with Python: A Step-by-Step Guide

Quick answer: To create an API client in Python, install the requests library, wrap the API’s base URL and authentication in a small class that uses a requests.Session, write one method per endpoint that sends the HTTP request and returns parsed JSON, and add error handling for bad status codes and timeouts. The whole thing fits in about 60 lines and becomes the foundation for any script that talks to a web service.

This step-by-step guide builds a real client against a free public API, then refactors it from a quick script into a reusable module with retries, pagination and tests. You will learn the structure professional developers use, the mistakes to avoid, and how to adapt the client to any REST API β€” GitHub, a network controller, a payments gateway or your own backend.

What an API client actually does

An API client is just code that hides the HTTP details behind friendly Python functions. Instead of remembering that users live at GET https://api.example.com/v1/users?page=2 with a bearer token in a header, the rest of your program calls client.list_users(page=2). A good client gives you:

  • One place for configuration β€” base URL, credentials, timeouts.
  • Consistent error handling β€” every call checks status codes the same way.
  • Readable call sites β€” business logic reads like English, not like a curl command.
  • Easy testing β€” you can mock the client without hitting the network.

Step 1: set up the environment

Use Python 3.9 or later. Create a virtual environment so the project’s dependencies stay isolated (python -m venv .venv, then activate it), and install the library with pip install requests.

We will use JSONPlaceholder, a free fake REST API that needs no sign-up, returns realistic JSON and accepts (but does not persist) writes. It is perfect for practising without risk.

Step 2: the five-line version

Start with the simplest thing that works, so you understand the moving parts before adding structure:

import requests

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

if response.status_code == 200:
    for user in response.json():
        print(f'{user["name"]:<25} {user["email"]}')
else:
    print("Request failed:", response.status_code, response.text[:200])

What happens here: requests.get() opens an HTTPS connection, sends the request and returns a Response object. status_code tells you whether the server was happy (200) and .json() converts the JSON body into Python lists and dictionaries. The timeout argument is not optional in real code β€” without it a dead server can freeze your script forever.

Step 3: build a reusable client class

The script above is fine once; it is painful the tenth time. Here is the same logic as a class you can import anywhere:

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


class APIError(Exception):
    """Raised when the API returns a non-success status code."""


class PlaceholderClient:
    def __init__(self, base_url="https://jsonplaceholder.typicode.com",
                 token=None, timeout=10):
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout

        self.session = requests.Session()
        self.session.headers.update({"Accept": "application/json",
                                     "User-Agent": "tkh-api-client/1.0"})
        if token:
            self.session.headers["Authorization"] = f"Bearer {token}"

        # Retry transient failures (5xx, 429) with exponential back-off
        retry = Retry(total=3, backoff_factor=0.5,
                      status_forcelist=[429, 500, 502, 503, 504],
                      allowed_methods=["GET", "PUT", "DELETE"])
        self.session.mount("https://", HTTPAdapter(max_retries=retry))

    def _request(self, method, path, **kwargs):
        url = f"{self.base_url}/{path.lstrip('/')}"
        resp = self.session.request(method, url, timeout=self.timeout, **kwargs)
        if not resp.ok:                       # anything outside 200-399
            raise APIError(f"{method} {url} -> {resp.status_code}: {resp.text[:200]}")
        return resp.json() if resp.content else None

    # --- public methods: one per endpoint ---
    def list_users(self):
        return self._request("GET", "/users")

    def get_user(self, user_id):
        return self._request("GET", f"/users/{user_id}")

    def list_posts(self, user_id=None):
        params = {"userId": user_id} if user_id else None
        return self._request("GET", "/posts", params=params)

    def create_post(self, user_id, title, body):
        payload = {"userId": user_id, "title": title, "body": body}
        return self._request("POST", "/posts", json=payload)

    def update_post(self, post_id, **fields):
        return self._request("PATCH", f"/posts/{post_id}", json=fields)

    def delete_post(self, post_id):
        return self._request("DELETE", f"/posts/{post_id}")


if __name__ == "__main__":
    client = PlaceholderClient(token=os.environ.get("API_TOKEN"))

    user = client.get_user(1)
    print("User:", user["name"], "-", user["company"]["name"])

    posts = client.list_posts(user_id=1)
    print(f"{len(posts)} posts by this user")

    new_post = client.create_post(1, "Hello from Techknowledgehub", "Learning APIs with Python")
    print("Created post id:", new_post["id"])

    try:
        client.get_user(9999)
    except APIError as e:
        print("Handled error:", e)

Walk through the important design choices:

  • Session reuses one TCP connection and applies headers once, so every method stays short.
  • _request() is the single funnel for all HTTP traffic. Logging, retries and error handling live here, not scattered across twenty methods.
  • Custom APIError lets callers catch API problems specifically instead of a generic Exception.
  • Retry automatically re-sends on 429 and 5xx responses with increasing delays β€” essential against rate-limited APIs.
  • The token comes from the environment, never from a literal in the file.

Choosing the right HTTP method

You want to… Method requests call Send data as
Read one or many records GET session.get(url, params=...) Query string
Create a record POST session.post(url, json=...) JSON body
Replace a record completely PUT session.put(url, json=...) JSON body
Change a few fields PATCH session.patch(url, json=...) JSON body
Remove a record DELETE session.delete(url) Nothing

Use the json= argument rather than data=json.dumps(...); requests serialises the dictionary and sets Content-Type: application/json for you.

Handling pagination

Real APIs rarely return everything at once. The usual patterns are page numbers (?page=2), offsets (?offset=100&limit=50) or a next URL in the response. A generator keeps memory low and the call site clean:

def iter_all(self, path, page_size=100):
    page = 1
    while True:
        batch = self._request("GET", path, params={"_page": page, "_limit": page_size})
        if not batch:
            break
        yield from batch
        page += 1

# usage: for comment in client.iter_all("/comments"): ...

The same approach is how you pull every device from a network controller or every repository from GitHub. If you plan to work with network APIs specifically, Using REST APIs for Seamless Network Device Interaction shows this client pattern applied to Cisco equipment.

Eight mistakes that break API clients

  1. No timeout. Always pass timeout=; a hung connection is the most common reason automation “stops working” overnight.
  2. Trusting the body without checking the status. Error responses are often valid JSON. Check resp.ok or call raise_for_status() first.
  3. Credentials in source control. Use environment variables or a secrets manager; add .env to .gitignore.
  4. Creating a new connection per call. Use a Session; it is faster and keeps headers consistent.
  5. Retrying non-idempotent POSTs blindly. A retried “create order” can create two orders. Retry GET, PUT and DELETE; think carefully about POST.
  6. Ignoring rate limits. Respect 429 responses and the Retry-After header rather than hammering the server.
  7. Printing full responses that contain secrets. Truncate and redact when logging.
  8. Skipping tests. Use the responses library or unittest.mock to fake HTTP so your tests run offline and fast.

Frequently asked questions

Should I use requests or httpx?

requests is the most widely used and perfectly good for synchronous scripts. httpx has an almost identical interface plus async support, which helps when you must call hundreds of endpoints concurrently. Start with requests; switching later is easy.

How do I handle authentication that expires?

Store the token and its expiry time on the client. In _request(), refresh the token when it is about to expire or when you receive a 401, then retry once.

Can I generate a client automatically from an OpenAPI spec?

Yes β€” tools such as openapi-python-client produce typed clients from a Swagger/OpenAPI file. They are great for large APIs, but understanding the hand-written version above helps you debug whatever they generate.

Key takeaways

  • An API client wraps HTTP calls in clean Python methods with one shared place for configuration, retries and errors.
  • Always set timeouts, check status codes, and keep credentials in environment variables.
  • Use a requests.Session, a custom exception and a generator for pagination.
  • The pattern is universal: once you have built one client, every REST API feels familiar.

Want to go beyond scripts and build data-driven applications in Python? The Techknowledgehub Data Science course covers Python fundamentals, working with APIs and JSON, pandas, visualisation and machine learning, with live mentor sessions and placement assistance. For free tutorials, subscribe to our YouTube channel.

JG
Written byJaya Gupta

Part of the Techknowledgehub team of industry mentors, writing practical guides to help you build a job-ready tech career.

More articles by Jaya Gupta β†’
Keep reading

Related articles

M# Programming Language
Languages

M# Programming Language

What the M# programming language really is, how its declarative models generate C# and ASP.NET Core code, who uses it,…

01 Apr 2023Β· 8 min read

Leave a Reply