ZetCode

Python GET/POST request

last modified October 30, 2026 Python 3.15

In this article we show how to send GET and POST requests in Python. In the examples, we use the standard urllib.request and socket modules and the urllib3 and requests libraries. We also show how to process GET and POST requests in Flask.

HTTP

The Hypertext Transfer Protocol (HTTP) is an application protocol for distributed, collaborative, hypermedia information systems. HTTP is the foundation of data communication for the World Wide Web.

In the examples, we use httpbin.org, which is a freely available HTTP request and response service; it returns the data of our request as JSON. We also use example.com, a small HTML page reserved for documentation examples.

HTTP GET

The HTTP GET method requests a representation of the specified resource.

GET requests:

HTTP POST

The HTTP POST method sends data to the server in the body of the request. It is often used when uploading a file or when submitting a completed web form.

POST requests:

Installing the libraries

The urllib.request and socket modules are part of the standard library. urllib3 is a powerful HTTP client with connection pooling and retries. requests is a simple and elegant HTTP library built on top of urllib3. Flask is a popular micro web framework based on the Werkzeug WSGI toolkit and the Jinja template engine.

$ pip install urllib3 requests Flask

GET request with socket

HTTP is a text protocol on top of TCP. To understand what the libraries do for us, we first send a GET request through a plain socket.

socket_get.py
import socket

host = 'example.com'
request = (
    'GET / HTTP/1.0\r\n'
    f'Host: {host}\r\n'
    'Accept: text/html\r\n'
    '\r\n'
)

with socket.create_connection((host, 80), timeout=10) as s:
    s.sendall(request.encode('ascii'))

    chunks = []
    while data := s.recv(4096):
        chunks.append(data)

response = b''.join(chunks).decode()
head, _, body = response.partition('\r\n\r\n')

print(head)
print()
print(body[:60])

We connect to port 80 of the server and send the request line, the Host header, and an empty line, which ends the headers. Lines are terminated with \r\n. We use HTTP/1.0, so the server closes the connection after the response and does not use chunked encoding. We read the data until recv returns an empty bytes object and split the response into the head and the body. We print the status line, the response headers, and the beginning of the HTML document.

$ python socket_get.py
HTTP/1.1 200 OK
Date: Fri, 30 Oct 2026 20:24:01 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Server: cloudflare
Last-Modified: Thu, 29 Oct 2026 13:15:03 GMT
Allow: GET, HEAD
Accept-Ranges: bytes
Age: 2230
cf-cache-status: HIT
CF-RAY: a52d24227804b379-PRG
alt-svc: h3=":443"; ma=86400

<!doctype html><html lang=en><head><meta charset=utf-8><link

GET request with urllib.request

The urlopen function from the standard urllib.request module sends a GET request by default.

urllib_get.py
from urllib.request import urlopen

with urlopen('https://example.com', timeout=10) as resp:

    print(resp.status, resp.reason)
    print(resp.headers['Content-Type'])

    html = resp.read().decode('utf-8')

start = html.index('<title>') + len('<title>')
end = html.index('</title>')
print(html[start:end])

The returned response object is a context manager. We print the status code, the reason phrase, and the content type, and then read the body. The read method returns bytes, which we decode to a string. From the HTML document, we extract the title.

$ python urllib_get.py
200 OK
text/html; charset=utf-8
Example Domain

Query parameters with urllib.request

The parameters of a GET request are sent in the query string of the URL. The urlencode function builds a properly encoded query string from a dictionary.

urllib_get_params.py
import json
from urllib.parse import urlencode
from urllib.request import urlopen

params = {'name': 'John Doe', 'occupation': 'gardener'}
url = 'https://httpbin.org/get?' + urlencode(params)

print(url)

with urlopen(url, timeout=10) as resp:
    data = json.load(resp)

print(data['args'])
print(data['url'])

The space in John Doe is encoded as a plus sign. The httpbin.org/get endpoint returns the parsed parameters in the args field. The json.load function parses the JSON body directly from the response object.

$ python urllib_get_params.py
https://httpbin.org/get?name=John+Doe&occupation=gardener
{'name': 'John Doe', 'occupation': 'gardener'}
https://httpbin.org/get?name=John+Doe&occupation=gardener

POST request with urllib.request

To send a POST request with urllib.request, we create a Request object with the data.

urllib_post.py
import json
from urllib.parse import urlencode
from urllib.request import Request, urlopen

form = {'name': 'John Doe', 'occupation': 'gardener'}
body = urlencode(form).encode('ascii')

req = Request('https://httpbin.org/post', data=body, method='POST')

with urlopen(req, timeout=10) as resp:
    data = json.load(resp)

print(data['form'])
print(data['headers']['Content-Type'])

The form data is encoded with urlencode and converted to bytes. When a request has data, urlopen sets the application/x-www-form-urlencoded content type. The method parameter makes the method explicit. The httpbin.org/post endpoint returns the form data in the form field.

$ python urllib_post.py
{'name': 'John Doe', 'occupation': 'gardener'}
application/x-www-form-urlencoded

GET request with urllib3

The urllib3.request function sends a request using a default pool manager. For GET requests, the fields parameter is encoded into the query string.

urllib3_get.py
import urllib3

resp = urllib3.request('GET', 'https://httpbin.org/get',
                       fields={'name': 'John Doe', 'occupation': 'gardener'})

print(resp.status)
print(resp.headers['Content-Type'])

data = resp.json()
print(data['args'])
print(data['headers']['User-Agent'])

The response object has a json method, which decodes the JSON body. We print the status code, the content type, the query parameters received by the server, and the user agent sent by urllib3.

$ python urllib3_get.py
200
application/json
{'name': 'John Doe', 'occupation': 'gardener'}
python-urllib3/2.8.0

POST request with urllib3

A PoolManager reuses connections and lets us set defaults such as the timeout and the number of retries.

urllib3_post.py
import urllib3

http = urllib3.PoolManager(timeout=10.0, retries=2)

resp = http.request('POST', 'https://httpbin.org/post',
                    fields={'name': 'John Doe', 'occupation': 'gardener'},
                    encode_multipart=False)

print(resp.json()['form'])

resp = http.request('POST', 'https://httpbin.org/post',
                    json={'name': 'John Doe', 'tags': ['garden', 'flowers']})

data = resp.json()
print(data['json'])
print(data['headers']['Content-Type'])

For POST requests, fields are encoded as multipart/form-data by default; with encode_multipart=False, they are sent URL-encoded like a web form. The json parameter serializes a Python object to JSON and sets the application/json content type.

$ python urllib3_post.py
{'name': 'John Doe', 'occupation': 'gardener'}
{'name': 'John Doe', 'tags': ['garden', 'flowers']}
application/json

GET request with requests

A GET request is generated with the get function. The query parameters are passed in the params dictionary.

requests_get.py
import requests

params = {'name': 'John Doe', 'occupation': 'gardener'}
resp = requests.get('https://httpbin.org/get', params=params, timeout=10)

print(resp.status_code)
print(resp.url)
print(resp.json()['args'])

The url attribute shows the final URL with the encoded query string. It is a good practice to always set a timeout; by default, requests waits for the response indefinitely.

$ python requests_get.py
200
https://httpbin.org/get?name=John+Doe&occupation=gardener
{'name': 'John Doe', 'occupation': 'gardener'}

POST request with requests

A POST request is generated with the post function. Form data is passed in the data parameter, JSON data in the json parameter.

requests_post.py
import requests

url = 'https://httpbin.org/post'

resp = requests.post(url, data={'name': 'Peter'}, timeout=10)
data = resp.json()
print(data['form'])
print(data['headers']['Content-Type'])

resp = requests.post(url, json={'name': 'Peter', 'age': 34}, timeout=10)
data = resp.json()
print(data['json'])
print(data['headers']['Content-Type'])

The first request sends form data, the second one JSON data. The library sets the content type of the request accordingly. httpbin.org returns the JSON keys in alphabetical order.

$ python requests_post.py
{'name': 'Peter'}
application/x-www-form-urlencoded
{'age': 34, 'name': 'Peter'}
application/json

Handling HTTP errors with requests

The requests library does not raise an exception for error status codes. The raise_for_status method raises an HTTPError if the status code is 4xx or 5xx.

requests_errors.py
import requests

for status in (200, 404, 503):

    resp = requests.get(f'https://httpbin.org/status/{status}', timeout=10)

    try:
        resp.raise_for_status()
        print(status, 'ok')
    except requests.HTTPError as e:
        print(e)

The httpbin.org/status/{code} endpoint returns the given status code. The error message contains the status code, the reason, and the URL.

$ python requests_errors.py
200 ok
404 Client Error: Not Found for url: https://httpbin.org/status/404
503 Server Error: Service Unavailable for url: https://httpbin.org/status/503

Process GET request in Flask

The following example shows how to process a GET request in a Flask application.

flask_get/app.py
from flask import Flask, request

app = Flask(__name__)


@app.route('/')
def index():
    return 'Home page'


@app.get('/greet')
def greet():
    name = request.args.get('name', 'Guest')
    msg = f'Hello {name}'
    return msg, 200, {'Content-Type': 'text/plain; charset=utf-8'}

The application creates and sends a message to the client. It uses the value from the name query parameter, which is available in the request.args dictionary; if the parameter is missing, the default value Guest is used. The app.get decorator maps the greet function to GET requests for the /greet path. The function returns the body, the status code, and the headers.

We start the development server in the background and send GET requests with the curl tool. With the -i option, curl also prints the response headers. The server logs each request.

$ flask --app flask_get/app run &
 * Serving Flask app 'flask_get/app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
$ curl -i 'localhost:5000/greet?name=Lucia'
HTTP/1.1 200 OK
Server: Werkzeug/3.1.9 Python/3.15.2
Date: Fri, 30 Oct 2026 20:24:09 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 11
Connection: close

Hello Lucia
127.0.0.1 - - [30/Oct/2026 21:24:09] "GET /greet?name=Lucia HTTP/1.1" 200 -
$ curl localhost:5000/greet
Hello Guest
127.0.0.1 - - [30/Oct/2026 21:24:09] "GET /greet HTTP/1.1" 200 -

Process POST request in Flask

The following example shows how to process a POST request in Flask. The application accepts both form and JSON data.

flask_post/app.py
from flask import Flask, request

app = Flask(__name__)

users = []


@app.post('/users')
def create_user():

    if request.is_json:
        user = request.get_json()
    else:
        user = request.form.to_dict()

    if 'name' not in user:
        return {'error': 'name is required'}, 400

    users.append(user)
    return user, 201


@app.get('/users')
def list_users():
    return users

The app.post decorator maps create_user to POST requests for the /users path. If the request has a JSON content type, we read the body with get_json; otherwise, we read the form data from request.form. Without a name, the function returns an error with the 400 status code. A new user is returned with the 201 Created status code. Flask converts the returned dictionaries and lists to JSON responses.

We send form data with the -d option of curl, which makes it a POST request, and JSON data with the matching content type header. The third request is rejected because the name is missing. Finally, a GET request lists the created users.

$ flask --app flask_post/app run &
 * Serving Flask app 'flask_post/app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
$ curl -i -d name=Peter -d occupation=driver localhost:5000/users
HTTP/1.1 201 CREATED
Server: Werkzeug/3.1.9 Python/3.15.2
Date: Fri, 30 Oct 2026 20:24:10 GMT
Content-Type: application/json
Content-Length: 39
Connection: close

{"name":"Peter","occupation":"driver"}
127.0.0.1 - - [30/Oct/2026 21:24:10] "POST /users HTTP/1.1" 201 -
$ curl -H 'Content-Type: application/json' -d '{"name": "Lucia", "occupation": "teacher"}' localhost:5000/users
{"name":"Lucia","occupation":"teacher"}
127.0.0.1 - - [30/Oct/2026 21:24:10] "POST /users HTTP/1.1" 201 -
$ curl -d occupation=pilot localhost:5000/users
{"error":"name is required"}
127.0.0.1 - - [30/Oct/2026 21:24:11] "POST /users HTTP/1.1" 400 -
$ curl localhost:5000/users
[{"name":"Peter","occupation":"driver"},{"name":"Lucia","occupation":"teacher"}]
127.0.0.1 - - [30/Oct/2026 21:24:11] "GET /users HTTP/1.1" 200 -

Source

urllib.request - Python documentation

Requests documentation

In this article we have generated GET and POST requests in Python and processed them in Flask.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.