This guide is about how to use REST APIs, which are a type of web API, with Python.
What is an API?
An API (Application Programming Interface) lets two programs talk to each other. You send a request. The server sends a response. This is called the request/response cycle.
Web APIs and other API types
A web API is an API that works over HTTP. HTTP is the protocol that defines how requests and responses are sent between browsers and servers on the web. Not all APIs use HTTP; some work inside operating systems, desktop apps, or databases without any web protocol.
Common web API styles are REST, SOAP, GraphQL, and gRPC. They solve similar problems but use different rules and formats.
REST stands for Representational State Transfer. It uses URLs as resources and standard HTTP verbs. Example: a weather app calling a public API.
SOAP stands for Simple Object Access Protocol. It uses strict XML messages and fixed rules about how the message should look. Fixed rules mean the tags and order should not change. Example: old bank or insurance systems.
GraphQL stands for Graph Query Language. It lets the client request only the fields it needs. Example: a mobile app loading a user profile and only a few fields.
gRPC stands for Google Remote Procedure Call. It was created by Google but is open source. The name does not limit where it is used. It uses binary messages and HTTP/2. Binary messages are compact data made for machines. Example: internal services inside a company talking to each other.
Web API parts: Protocol, Domain, Port, Path, Query
Protocol: https and :// show where the address starts
Domain: jsonplaceholder.typicode.com
Port: :443 (often hidden)
Path: /posts
Query: ?userId=1
Protocol shows the rules for the request, like http or https. It tells the browser and server how to talk, for example whether data is encrypted. Domain tells where the server is. Port is used when the server listens on a specific channel. Think of it as a numbered door to the same server, like :3000. It is often hidden when it is default. Path points to a resource, like /posts or /posts/1. Query is used for filters and options. A filter narrows results, an option changes how results are returned.
API requests and responses
For web APIs,
an API request is a URL plus an HTTP verb. The verb is the action, like GET or POST.
an API response is what the server sends back. It has a status code and data, often in JSON.
Each request and response has a start-line, headers, and a body.
The start-line shows the method and path (request) or the status code (response), e.g., GET /posts/1 or HTTP/1.1 200 OK.
Headers carry metadata like content type, e.g., Content-Type: application/json.
The body carries the data payload when present, e.g., a JSON object.
HTTP verbs
A REST API uses HTTP verbs to show intent:
GET: Read data
POST: Create data
PUT: Update data (replace)
PATCH: Update data (partial)
DELETE: Remove data
HTTP status messages
HTTP status messages are short signals in the response. They tell if the request worked or failed.
JSONPlaceholder is a fake REST API for practice. It returns sample data, is free, and needs no authentication. Data is not saved after POST or PUT, but the responses still show how the API would behave. It supports GET, POST, PUT, PATCH and DELETE.
GET
A GET request retrieves data from the server.
import requestsurl ="https://jsonplaceholder.typicode.com/posts"# set the endpoint urlresponse = requests.get(url) # send the requestprint(response.status_code) # show the http status code if okprint(response.reason) # show the http status reason
200
OK
The status code 200 means the request worked.
Now let’s see the response text.
print(len(response.text)) # show the length of the response textprint(response.text[:500]) # preview the first 500 characters of the response text
27520
[
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
},
{
"userId": 1,
"id": 2,
"title": "qui est esse",
"body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate p
Reading JSON from GET responses
When an API is called, the server can respond in different ways. It is often a database query, but not always. It can be:
a database query returns JSON
cached data is returned
another service is called and the result is forwarded
a file or static JSON is read
Many APIs return raw text formatted as JSON. The requests package can decode that text into Python objects, like lists and dictionaries.
Parse JSON into a Python list and show the type:
data = response.json()print(type(data))
<class 'list'>
Show the number of items in the list:
print(len(data))
100
Show the first 2 items in the list:
print(data[:2])
[{'userId': 1, 'id': 1, 'title': 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit', 'body': 'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto'}, {'userId': 1, 'id': 2, 'title': 'qui est esse', 'body': 'est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla'}]
Show the keys of the first item in the list:
print(data[0].keys())
dict_keys(['userId', 'id', 'title', 'body'])
Read a field from the first item:
print(data[0]["title"])
sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Query parameters
Query parameters refine what is asked for by adding filters or options.
Filter posts by userId:
params = {"userId": 1} # set query parametersurl ="https://jsonplaceholder.typicode.com/posts"# set the collection endpointresponse = requests.get(url, params=params) # send the request with params
print("The user with Id 1 has ", len(response.json()), "posts")
The user with Id 1 has 10 posts
Other HTTP verbs
POST
POST request creates new data on the server.
url ="https://jsonplaceholder.typicode.com/posts"payload = {"title": "hello", "body": "first post", "userId": 1} # set the request bodyresponse = requests.post(url, json=payload)print(response.status_code)print(response.reason)print(response.json()) # show the created object
PUT fully updates existing data on the server.
The response depends on the API design: 200 with a body, 204 with no body, or just a status message may be returned.
Some APIs return the updated object by default, or allow it via a parameter or header.
In JSONPlaceholder, a simulated updated object is returned.
{'userId': 1, 'id': 1, 'title': 'patched title', 'body': 'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto'}
Headers are key-value pairs that carry extra info about the request and response.
The accept header says which format is requested, and content-type says which format is sent or returned.
This back-and-forth is called content negotiation.
Common headers:
accept: application/json –> sent by client to request a format
content-type: application/json –> sent by client or server to say the data format
authorization: Bearer <token> –> sent by client to prove identity
user-agent: Mozilla/5.0 –> sent by client to identify the app
cache-control: no-cache –> sent by client or server to avoid using old cached data
print(response.headers["date"]) # show the response dateprint(response.headers["server"]) # show the server softwareprint(response.headers["connection"]) # show the connection typeprint(response.headers.get("cache-control")) # show the cache controlprint(response.headers.get("content-length")) # show the content length
Status code handling
The requests package has a built-in helper object named requests.codes, which maps common status names to numbers for cleaner response handling.
url ="https://jsonplaceholder.typicode.com/posts/1"response = requests.get(url)print(response.reason)if response.status_code == requests.codes.ok: # check for 200print("success")elif response.status_code == requests.codes.not_found: # check for 404print("not found")
OK
success
Error handling
Errors can happen at the network level or the HTTP level. Both can be handled with try/except.
The imports below bring specific exception classes (error types) into the current namespace.
This allows except ConnectionError: and except HTTPError: to be recognized.
If they are not imported, ConnectionError / HTTPError may be undefined in the script.
from requests.exceptions importConnectionError# import network errorfrom requests.exceptions import HTTPError # import http errortry:# Send an HTTP request to the server/URL (knock on the door).# If it succeeds, a "response" object is sent back. response = requests.get(url)# If the HTTP status code is 4xx or 5xx, requests *raises* (throws) an HTTPError exception (an error object).# If it's 2xx/3xx, nothing is raised and the code continues. response.raise_for_status()# If we reached this line, no exception was raised, it prints the status code.print(response.status_code)exceptConnectionErroras conn_err:# This block is entered if a ConnectionError-type exception is raised inside the try block.# The match is not performed by text/title; it is performed by TYPE:# (roughly) isinstance(raised_error, ConnectionError) must be True.# A ConnectionError usually indicates that no HTTP response was received at all # (e.g., no internet, DNS failure, server unreachable, connection refused).# "as conn_err" is used to bind the raised exception object to the variable `conn_err`,# so its details can be printed.print(f"connection error: {conn_err}")except HTTPError as http_err:# This runs if an HTTPError-type exception was raised inside the try block.# This is triggered by response.raise_for_status() when the server responds with 4xx/5xx (e.g., 404, 500).print(f"http error: {http_err}")
200
Rate limits
Some APIs limit how many requests can be sent in a short time.
A short delay is often used to avoid errors such as 429 (Too Many Requests).
import time # import time toolsfor _ inrange(3): response = requests.get("https://api.example.com/resource")print(response.status_code) time.sleep(10) # wait 10 second between requests
Authentication overview
Authentication is the way a client proves who it is.
It is needed when data is private, when write actions are allowed, or when usage limits are enforced.
Common types:
basic auth: username and password are sent on each request
bearer token: a token is sent in the authorization header
oauth: a token is issued after a user grants access
OAuth is used when a user needs to grant access without sharing a password.
A token is issued after permission is given.
Example: access to Google Photos is granted to a photo app after approval.
Example: access to Microsoft Outlook is granted to a task app after approval.
An API key example is shown in the next section with NASA DEMO_KEY.
API key example with NASA API
NASA provides a public demo key for quick tests. The key is passed as a query parameter. For details, see: https://api.nasa.gov/. In the URL below, planetary is the NASA API section for space and planets and APOD means Astronomy Picture of the Day.
200
{'copyright': 'Roi Levi', 'date': '2026-01-01', 'explanation': "Cycle 25 solar maximum made 2025 a great year for aurora borealis (or aurora australis) on planet Earth. And the high level of solar activity should extend into 2026. So, while you're celebrating the arrival of the new year, check out this spectacular auroral display that erupted in starry night skies over Kirkjufell, Iceland. The awesome auroral corona, energetic curtains of light streaming from directly overhead, was witnessed during a strong geomagnetic storm triggered by intense solar activity near the March 2025 equinox. This northland and skyscape captures the evocative display in a 21 frame panoramic mosaic.", 'hdurl': 'https://apod.nasa.gov/apod/image/2601/AuroraFireworksstormRoiLevi.jpg', 'media_type': 'image', 'service_version': 'v1', 'title': 'Auroral Corona', 'url': 'https://apod.nasa.gov/apod/image/2601/AuroraFireworksstormRoiLevi1024.jpg'}
data = response.json() # parse JSON into a Python dictprint(data.keys()) # show the keys in the dictprint(data.get("title")) # show the image titleprint(data.get("media_type")) # show the media typeprint(data.get("url")) # show the media url
The response contains the image URL, not the image file. The URL can be used to show the image. The media_type can be image or video, so a check is used before showing the image.
from IPython.display import Image, display # import display helpers# display(Image(url=data.get("url"), width=400)) # show a smaller imageif data.get("media_type") =="image": display(Image(url=data.get("url"), width=400))else:print("media is not an image")
BACKUP: List all HTTP status codes and phrases
from http import HTTPStatusfor s in HTTPStatus:print(s.value, s.phrase)
100 Continue
101 Switching Protocols
102 Processing
103 Early Hints
200 OK
201 Created
202 Accepted
203 Non-Authoritative Information
204 No Content
205 Reset Content
206 Partial Content
207 Multi-Status
208 Already Reported
226 IM Used
300 Multiple Choices
301 Moved Permanently
302 Found
303 See Other
304 Not Modified
305 Use Proxy
307 Temporary Redirect
308 Permanent Redirect
400 Bad Request
401 Unauthorized
402 Payment Required
403 Forbidden
404 Not Found
405 Method Not Allowed
406 Not Acceptable
407 Proxy Authentication Required
408 Request Timeout
409 Conflict
410 Gone
411 Length Required
412 Precondition Failed
413 Request Entity Too Large
414 Request-URI Too Long
415 Unsupported Media Type
416 Requested Range Not Satisfiable
417 Expectation Failed
418 I'm a Teapot
421 Misdirected Request
422 Unprocessable Entity
423 Locked
424 Failed Dependency
425 Too Early
426 Upgrade Required
428 Precondition Required
429 Too Many Requests
431 Request Header Fields Too Large
451 Unavailable For Legal Reasons
500 Internal Server Error
501 Not Implemented
502 Bad Gateway
503 Service Unavailable
504 Gateway Timeout
505 HTTP Version Not Supported
506 Variant Also Negotiates
507 Insufficient Storage
508 Loop Detected
510 Not Extended
511 Network Authentication Required