Support

How to Call an API in Python: A Complete Beginner's Guide

By Simon O'Connor · Updated 18 June 2026 · 14 min read

Most useful programs need data from somewhere else: weather, songs, stock prices, user accounts, saved records. An API is how your Python code asks another system for that data and gets a structured response back.

In practice, calling an API means sending an HTTP request to a URL and reading the response. The request may include parameters, headers, or JSON data. The response usually includes a status code and a body your program can inspect.

This guide walks through that process from the first live request to GET and POST calls, query parameters, headers, JSON responses, and basic error checks. It assumes Python 3.10 or later and one library: requests.

What you need

You need Python 3.10 or later, and one library called requests. It is not part of the standard library, but it is the package almost everyone reaches for, because it makes HTTP calls readable. Install it with Python's package installer.

Bash
python -m pip install requests

Running pip through python -m makes sure the package installs into the same Python you will use to run the script. If you have used virtual environments before, create and activate one first so this install stays scoped to the current project rather than your whole machine. If that sentence means nothing yet, do not worry, the command above works fine for following along.

Your first API call

We will start against JSONPlaceholder, a free public API built specifically for examples like this one. It needs no key, no signup, and it is stable, so the code below will behave the same whenever you run it. Here is a complete first call.

first_call.py
import requests

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

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

Three lines do the work. The import requests brings in the library. The requests.get(...) call sends an HTTP GET request to that URL and waits for the answer, which it hands back as a response object we store in response. The timeout=10 says give up after ten seconds rather than waiting forever, and we will come back to why that small argument matters more than it looks.

The last two lines read the answer. response.status_code is a number the server sends to say how the request went, and for a successful call it is 200. response.json() takes the response body, which arrived as text, and parses it into a Python object you can work with. This particular endpoint returns a small JSON object representing a single to-do item, so what you get back is a dictionary.

Reading the response

The response object carries the answer in more than one form, and choosing the right one matters. response.text gives you the raw body exactly as it arrived, as a string. response.json() goes one step further and parses that string into Python data, turning a JSON object into a dictionary and a JSON array into a list. For any API that returns JSON, which is most of them, response.json() is what you want.

Python
import requests

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

print(data["title"])
print(data["completed"])

Once response.json() has handed you a dictionary, you read fields out of it the same way you read any Python dictionary, by key. The exact keys depend entirely on the API, and the only reliable way to learn them is to print the parsed data once and look, or to read the API's documentation. Reach for response.text only when the body is not JSON, or when a parse fails and you want to see the raw bytes the server actually sent.

Sending query parameters

Most real requests need to be more specific than "give me everything". You narrow them with query parameters, the key=value pairs that appear after a ? in a URL. You could build that string by hand, but it is fiddly and easy to get wrong once values contain spaces or symbols. Pass a dictionary to params= instead and let requests assemble the URL correctly.

Python
import requests

response = requests.get(
    "https://jsonplaceholder.typicode.com/comments",
    params={"postId": 1},
    timeout=10,
)

print(response.url)
print(len(response.json()))

Here requests turns {"postId": 1} into ?postId=1 on the end of the URL, which you can confirm by printing response.url. The payoff grows with the request: add three more filters and you simply add three more keys to the dictionary, with the encoding handled for you. This keeps your code readable and spares you a whole category of subtle bugs around special characters.

Headers and authentication

JSONPlaceholder is open to anyone, but most real APIs need to know who is asking. They issue you an API key, a long secret string, and you prove your identity by sending it with each request. The most common place to put it is an HTTP header, very often as Authorization: Bearer <your key>. You pass headers as a dictionary to headers=.

Python
import os
import requests

# Set this before running: export API_KEY=... (macOS/Linux) or $env:API_KEY="..." (Windows PowerShell)
api_key = os.environ["API_KEY"]
headers = {"Authorization": f"Bearer {api_key}"}

# Illustrative endpoint and header shape only.
response = requests.get(
    "https://api.example.com/v1/data",
    headers=headers,
    timeout=10,
)

The exact header name and format vary by provider, so always check the API's documentation. Some APIs expect the key as a query parameter instead, which you would pass through params= exactly as in the previous section. The endpoint above is illustrative, a placeholder for whatever real API you end up using. Reading the key with os.environ["API_KEY"] also means the script fails immediately if the key is missing, rather than sending a broken Authorization header.

One rule, though, holds for every API. Never paste your real key directly into your code as a literal string. A hardcoded key leaks the moment you share the file or push it to a public repository, and rotating it afterwards is a chore. Keep secrets out of source code by loading them from the environment. Our guide to storing API keys with a .env file shows the standard, safe pattern end to end.

Status codes: did it work?

Every response carries a status code, and its first digit tells you almost everything. A 2xx code such as 200 means success. A 4xx code such as 404 Not Found or 401 Unauthorized means your request was wrong, so fixing the request is the only thing that will help. A 5xx code such as 503 means the server itself failed, which is usually temporary and on their side, not yours.

Checking the number yourself works, but requests gives you a shortcut. Calling response.raise_for_status() does nothing on a 2xx and raises an exception on any 4xx or 5xx, so a bad status stops your program loudly instead of slipping through silently.

Python
import requests

response = requests.get("https://jsonplaceholder.typicode.com/todos/1", timeout=10)
response.raise_for_status()  # raises on a 4xx or 5xx
data = response.json()

Be aware that a 200 is not always a true success, since some APIs return an error message wrapped in a perfectly successful status, and raise_for_status() will not catch that. Knowing how to tell apart the layers where a call can fail, and how to fail gracefully rather than crash, is worth its own read. Our guide to handling API errors in Python covers status codes, the exception hierarchy, and graceful failure in full.

Sending data with POST

So far every call has fetched data. To send data, to create or change something on the server, you use a different HTTP method: POST. The distinction is worth holding onto. A GET reads and changes nothing, like asking the waiter what is on the menu. A POST sends something and may change the world, like placing an order. With requests, you send a JSON body by passing a dictionary to json=.

Python
import requests

new_post = {"title": "Hello", "body": "My first post", "userId": 1}

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

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

Passing the dictionary through json= does two helpful things automatically. It serialises the dictionary to a JSON string for the body, and it sets the Content-Type header to application/json so the server knows how to read it. JSONPlaceholder is a fake API that does not really store anything, but it plays along, echoing your object back with a freshly assigned id and a 201 status, which is the conventional code for "created".

Making it reliable

Notice the timeout=10 on every call so far. That is deliberate, and it matters more than any other single habit on this page. By default requests has no timeout and no retries at all. If the server accepts your connection and then goes quiet, your program waits forever, and a single network hiccup becomes an immediate failure. While you are learning, a failed script is not a disaster. In real code, hanging forever is dangerous, so always pass a timeout.

Timeouts are only the first habit. Production calls also retry the failures that are worth retrying, and they space those retries out so they do not pile onto a server that is already struggling. Our guide to timeouts, retries, and backoff walks through all three, with copy-paste code you can lift into a project.

The happy path is only the beginning

The call you have learned here is the happy path, where the network behaves and the server cooperates. Production-ready calls add three things on top: a timeout on every request, real error handling for when it fails, and a refusal to trust the response blindly. Treat the working call as a foundation, not the finished thing, and the deeper guides linked throughout fill in each layer.

Don't trust the shape blindly

There is one more assumption hiding in the happy path. When you write data["title"], you are trusting that the response contains a title field of the type you expect. That trust is often misplaced. An API can change its response between versions, omit a field for some records, or return an error object shaped nothing like the success object. The result is a confusing KeyError or TypeError far from the real cause, deep in code that assumed the data was fine.

The fix is to check the shape of the data at the boundary, the moment it arrives, before the rest of your program depends on it. Our guide to validating JSON with a schema shows how to catch a malformed or unexpected response early, with a clear error, instead of letting it surface as a baffling failure three functions later.

Putting it together

Here is a slightly larger example that combines what you have learned: a GET with query parameters, a timeout on the call, a status check with raise_for_status(), and reading a couple of fields out of the parsed response. This one still uses JSONPlaceholder, so you can run it as written.

comments.py
import requests

BASE_URL = "https://jsonplaceholder.typicode.com/comments"


def get_comments_for_post(post_id):
    response = requests.get(
        BASE_URL,
        params={"postId": post_id},
        timeout=10,
    )
    response.raise_for_status()
    return response.json()


comments = get_comments_for_post(1)

for comment in comments[:3]:
    if "email" in comment and "name" in comment:
        print(comment["email"], "-", comment["name"])

Read it top to bottom and the shape of a real API call comes through. We build the request with parameters and a timeout, we let raise_for_status() turn a bad status into a clear error rather than a silent one, and only then do we parse the body and pull out the fields we came for. In your own projects, the URL, parameter names, and response fields come from the API's documentation.

Where to go next

You can now make real API calls. The four guides below each take one piece of the production picture and go deep, and together they turn a working call into one you can trust.

Frequently asked questions

What library should I use to call an API in Python?

For most beginner and everyday scripts, requests. It reads cleanly, has excellent documentation, and the wider Python ecosystem knows it well. Python's standard library does include urllib, which can make HTTP calls with no install, but it is more verbose and harder to read. If you need asynchronous requests for concurrency, httpx is a modern alternative with a very similar interface. Start with requests and reach for the others only when a specific need points you there.

What is the difference between GET and POST?

A GET reads data and should not change anything on the server, so it is the method you use for fetching. A POST sends data to create or change something, such as adding a record or submitting a form, so repeating it may have real effects like a duplicate entry. As a rule of thumb, use GET to fetch and POST to send. In requests that is the difference between requests.get(...) and requests.post(..., json=...).

Do I need an API key to call an API?

It depends on the API. Many public APIs, like the JSONPlaceholder one used throughout this guide, need no key at all. Most real, production APIs do require one, usually sent in a header such as Authorization: Bearer <key> or occasionally as a query parameter. When a key is required, keep it out of your source code and load it from the environment instead, so it never leaks through a shared file or a public repository.


Next Step:

You can now make a request, check the status, and read the JSON. Chapter 3 of Mastering APIs with Python covers this same ground in more depth, and it is free to read in full. From there the book builds toward validation, OAuth, databases, testing, and deployment.