How to make HTTP requests using PyScript, in pure Python#

Pyodide, the interpreter that underlies PyScript, does not have the requests module (or other similar modules) available by default, which are traditionally used to make HTTP requests in Python. However, it is possible to make HTTP requests in Pyodide using the modern JavaScript fetch API (docs). This example shows how to make common HTTP request (GET, POST, PUT, DELETE) to an API, using only Python code! We will use asynchronous functions with async/await syntax, as concurrent code is preferred for HTTP requests.

The purpose of this guide is not to teach the basics of HTTP requests, but to show how to make them from PyScript using Python, since currently, the common tools such as requests and httpx are not available.

Fetch#

The fetch API is a modern way to make HTTP requests. It is available in all modern browsers, and in Pyodide.

Although there are two ways to use fetch:

  1. using JavaScript from PyScript

  2. using Pyodide’s Python wrapper, pyodide.http.pyfetch

This example will only show how to use the Python wrapper. Still, the fetch documentation is a useful reference, as its parameters can be called from Python using the pyfetch wrapper.

Pyodide.http, pyfetch, and FetchResponse#

The pyodide.http module is a Python API for dealing with HTTP requests. It provides the pyfetch function as a wrapper for the fetch API, which returns a FetchResponse object whenever a request is made. Extra keyword arguments can be passed to pyfetch which will be passed to the fetch API.

The returned object FetchResponse has familiar methods and properties for dealing with the response, such as json() or status. See the FetchResponse documentation for more information.

Example#

We will make async HTTP requests to JSONPlaceholder’s fake API using pyfetch. First we write a helper function in pure Python that makes a request and returns the response. This function makes it easier to make specific types of requests with the most common parameters.

Python convenience function#

from pyodide.http import pyfetch, FetchResponse
from typing import Optional, Any

async def request(url: str, method: str = "GET", body: Optional[str] = None,
                  headers: Optional[dict[str, str]] = None, **fetch_kwargs: Any) -> FetchResponse:
    """
    Async request function. Pass in Method and make sure to await!
    Parameters:
        url: str = URL to make request to
        method: str = {"GET", "POST", "PUT", "DELETE"} from `JavaScript` global fetch())
        body: str = body as json string. Example, body=json.dumps(my_dict)
        headers: dict[str, str] = header as dict, will be converted to string...
            Example, headers=json.dumps({"Content-Type": "application/json"})
        fetch_kwargs: Any = any other keyword arguments to pass to `pyfetch` (will be passed to `fetch`)
    Return:
        response: pyodide.http.FetchResponse = use with .status or await.json(), etc.
    """
    kwargs = {"method": method, "mode": "cors"}  # CORS: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
    if body and method not in ["GET", "HEAD"]:
        kwargs["body"] = body
    if headers:
        kwargs["headers"] = headers
    kwargs.update(fetch_kwargs)

    response = await pyfetch(url, **kwargs)
    return response

This function is a wrapper for pyfetch, which is a wrapper for the fetch API. It is a coroutine function, so it must be awaited. It also has type hints, which are not required, but are useful for IDEs and other tools. The basic idea is that the PyScript will import and call this function, then await the response. Therefore, the script containing this function must be importable by PyScript.

For this example, we will name the file containing the Python code request.py and place it in the same directory as the file containing the html code, which is described below.

PyScript HTML code#

In this How-to, the HTML code is split into separate code blocks to enable context highlighting (coloring of the Python code inside the html code block), but in reality it is all in the same file. The first part is a bare bones PyScript html page, using the community examples set-up. The second part is the actual Python code for HTTP requests, which is wrapped in <py-script> tags, while the third block has the concluding html code.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />

    <title>GET, POST, PUT, DELETE example</title>

    <link rel="icon" type="image/png" href="favicon.png" />
    <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />

    <script defer src="https://pyscript.net/latest/pyscript.js"></script>
    <py-config>
      [[fetch]]
      files = ["/request.py"]
    </py-config>
  </head>

  <body><p>
    Hello world request example! <br>
    Here is the output of your request:
    </p>
    <py-script>
        import asyncio
        import json
        from request import request  # import our request function.

        async def main():
            baseurl = "https://jsonplaceholder.typicode.com"

            # GET
            headers = {"Content-type": "application/json"}
            response = await request(f"{baseurl}/posts/2", method="GET", headers=headers)
            print(f"GET request=> status:{response.status}, json:{await response.json()}")

            # POST
            body = json.dumps({"title": "test_title", "body": "test body", "userId": 1})
            new_post = await request(f"{baseurl}/posts", body=body, method="POST", headers=headers)
            print(f"POST request=> status:{new_post.status}, json:{await new_post.json()}")

            # PUT
            body = json.dumps({"id": 1, "title": "test_title", "body": "test body", "userId": 2})
            new_post = await request(f"{baseurl}/posts/1", body=body, method="PUT", headers=headers)
            print(f"PUT request=> status:{new_post.status}, json:{await new_post.json()}")

            # DELETE
            new_post = await request(f"{baseurl}/posts/1", method="DELETE", headers=headers)
            print(f"DELETE request=> status:{new_post.status}, json:{await new_post.json()}")

        asyncio.ensure_future(main())
    </py-script>

    <div>
    <p>
        You can also use other methods. See fetch documentation: <br>
        https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters
    </p>
    </div>
    <div>
        <p>
        See pyodide documentation for what to do with a FetchResponse object: <br>
        https://pyodide.org/en/stable/usage/api/python-api.html#pyodide.http.FetchResponse
        </p>
    </div>
  </body>
</html>

Explanation#

py-config tag for importing our Python code#

The very first thing to notice is the py-config tag. This tag is used to import Python files into the PyScript. In this case, we are importing the request.py file, which contains the request function we wrote above.

py-script tag for making async HTTP requests.#

Next, the py-script tag contains the actual Python code where we import asyncio and json, which are required or helpful for the request function. The # GET, # POST, # PUT, # DELETE blocks show examples of how to use the request function to make basic HTTP requests. The await keyword is required not only for the request function, but also for certain methods of the FetchResponse object, such as json(), meaning that the code is asynchronous and slower requests will not block the faster ones.

HTTP Requests#

HTTP requests are a very common way to communicate with a server. They are used for everything from getting data from a database, to sending emails, to authorization, and more. Due to safety concerns, files loaded from the local file system are not accessible by PyScript. Therefore, the proper way to load data into PyScript is also through HTTP requests.

In our example, we show how to pass in a request body, headers, and specify the request method, in order to make GET, POST, PUT, and DELETE requests, although methods such as PATCH are also available. Additional parameters for the fetch API are also available, which can be specified as keyword arguments passed to our helper function or to pyfetch. See the fetch documentation for more information. HTTP requests are defined by standards-setting bodies in RFC 1945 and RFC 9110.

Conclusion#

This tutorial demonstrates how to make HTTP requests using pyfetch and the FetchResponse objects. Importing Python code/files into the PyScript using the py-config tag is also covered.

Although a simple example, the principals here can be used to create complex web applications inside of PyScript, or load data into PyScript for use by an application, all served as a static HTML page, which is pretty amazing!

API Quick Reference#

pyodide.http.pyfetch#

Usage#

await pyodide.http.pyfetch(url: str, **kwargs: Any) -> FetchResponse

Use pyfetch to make HTTP requests in PyScript. This is a wrapper around the fetch API. Returns a FetchResponse.

pyodide.http.FetchResponse#

Usage#

response: pyodide.http.FetchResponse = await <pyfetch call>
status = response.status
json = await response.json()

Class for handling HTTP responses. This is a wrapper around the JavaScript fetch Response. Contains common (async) methods and properties for handling HTTP responses, such as json(), url, status, headers, etc.