How to Call an API in Python: A Complete Beginner's Guide
Almost every useful program eventually needs data it does not own. Live weather, a list of songs, the price of a stock, a place to save a record. An API is how your program asks another program for that data, or asks it to do something, and gets a structured answer back. In Python, making that request is genuinely a few lines of code, and this guide walks you through every one of them.
Think of an API (an Application Programming Interface) as a waiter. You do not walk into the kitchen and cook. You give the waiter a clear order, the kitchen does the work, and the waiter brings back exactly what you asked for in a tidy form. Your code is the diner, the API is the waiter, and some server you will never see is the kitchen. You only ever talk to the waiter, using a small set of polite, predictable phrases.
By the end of this guide you will be able to make a real request to a live API, read the answer it sends back, pass it parameters, send it data of your own, and tell whether the call actually worked. We will also point you, at the right moments, to deeper guides on the few things that turn a working call into a reliable one. It assumes Python 3.10 or later and nothing else you have not already installed.
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 pip.
pip install requests
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 plain 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.
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.
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.
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=.
import requests
headers = {"Authorization": "Bearer YOUR_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.
One rule, though, holds for every API. Never paste your key directly into your code as a literal string the way the example does for clarity. 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.
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=.
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 turns a one-off failure into a crash. While you are learning, the defaults are fine. In real code they are 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. The base URL is an illustrative placeholder standing in for a real provider, so swap it for whichever API you use and adjust the field names to match its documentation.
import requests
# Illustrative base URL and fields; replace with a real provider's.
BASE_URL = "https://api.example.com/v1/weather"
def get_weather(city):
response = requests.get(
BASE_URL,
params={"q": city, "units": "metric"},
timeout=10,
)
response.raise_for_status()
data = response.json()
return data
weather = get_weather("Dublin")
print(weather["temperature"])
print(weather["description"])
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. The field names here are invented for illustration. Every API names its fields differently, so the print at the end is where you would confirm what the real response actually contains.
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.
- Timeouts, retries, and backoff covers why the defaults are dangerous and how to make every call resilient to a flaky network.
- Handling API errors covers status codes, the
requestsexception hierarchy, and how to fail gracefully instead of crashing. - Storing API keys with a .env file covers the standard, safe way to keep secrets out of your source code.
- Validating JSON with a schema covers checking a response's shape at the boundary so a changed payload fails clearly and early.
Frequently asked questions
What library should I use to call an API in Python?
For almost everyone, requests. It is the de facto standard, it reads cleanly, and the wider ecosystem assumes you are using it. 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 changes nothing on the server, so it is safe to repeat as often as you like. 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.
Mastering APIs with Python
Calling an API is the first step. Turning that into software you can trust, with real clients built against live services and made robust end to end, is the rest of the journey. In the full book, you build exactly that across six portfolio projects covering Flask, OAuth, SQLite, Postgres, Docker, CI/CD, and AWS. 30 chapters, start to finish.
Get the book for €35Chapter 3 is free to read.