8.4. Python Client Interface

VoltDB provides a client interface for programs written in Python. The Python client interface is packaged with VoltDB and is also available from PyPI via pip install voltdbclient. The distribution package includes two modules: the older voltdbclient module and the new voltclient module, which this section describes. To use the client from the package included in the VoltDB distribution, make sure the distribution's lib/python folder is on the Python module search path (for example, by adding it to the PYTHONPATH environment variable). The client requires Python 3.9 or later and has no third-party dependencies.

The new VoltDB Python client interface offers similar functionality to the Java client interface. The voltclient module provides a topology-aware client that maintains a connection to every node in the cluster, intelligently sends single-partitioned calls directly to the node responsible, supports asynchronous calls, and adjusts automatically to changes in the cluster — nodes failing, rejoining, or being added — without having to restart the application.

The following sections explain the four key steps when using the Python interface:

  1. Creating a client connection

  2. Invoking stored procedures (both synchronously and asynchronously)

  3. Interpreting the results

  4. Closing a client connection

8.4.1. Creating a Connection to the Database Cluster

Before you can call VoltDB stored procedures, you must create a client instance and connect to the database cluster. In the Python interface this is done through the class VoltClient. The following example imports VoltClient and creates a connection to the cluster containing the servers voltsvr1 and voltsvr2:

from voltclient import VoltClient

client = VoltClient(hosts=["voltsvr1", "voltsvr2"])

You do not need to list every server in the cluster. If the client is given at least one reachable node, it finds the rest automatically.

8.4.2. Invoking Stored Procedures

The Python client provides both a synchronous and asynchronous interface. In either case, to make stored procedure calls, you must first import VoltType from the voltclient module so you can declare the datatypes of the procedure arguments. To execute a stored procedure synchronously, use the call() method. The following example imports VoltType and calls the Vote procedure, which has three parameters — phone number, contestant number, and maximum votes allowed per phone number:

from voltclient import VoltType

response = client.call("Vote",
                       [VoltType.BIGINT, VoltType.INTEGER, VoltType.BIGINT],
                       [5551234567, 3, 2])

8.4.3. Invoking Stored Procedures Asynchronously

To make asynchronous procedure calls, use call_async(), which returns a standard Python concurrent.futures.Future (a placeholder for an eventual result). This allows your application to start many calls at once and collect the responses as the calls complete. The following example creates one Future per vote, then iterates through the results in the order they arrive:

import concurrent.futures

# Each vote is (phone number, contestant number)
votes = [
   (6175550101, 1),
   (6175550102, 3),
   (2125550199, 1),
   (2125550143, 6),
]

futures = [client.call_async("Vote",
                             [VoltType.BIGINT, VoltType.INTEGER, VoltType.BIGINT],
                             [num, contestant, 1])
           for num, contestant in votes]

for f in concurrent.futures.as_completed(futures):
   response = f.result()
   # ... check each response ...

8.4.4. Interpreting the Results

Both the synchronous and asynchronous invocations return a VoltResponse object that contains both the status of the call and the return values. A status of 1 indicates a success, while any other value indicates a problem. You can use the statusString attribute to display further information on problems encountered while running the stored procedure. For example:

if response.status != 1:
   print(f"Stored procedure failed: {response.statusString}")

An error that prevents a call from completing at all, like the connection being lost, raises a Python exception. The outcome of the call is uncertain, and the client will not resend it. The application must decide whether it is safe to make the same call a second time.

If the stored procedure is successful, you can use the client response to retrieve the results. The results are returned as a list of VoltTable objects via the tables attribute. Each table has a columns list describing the column names and datatypes, and a tuples list containing the rows. The values in each row are already converted to the corresponding Python datatypes (for instance, VoltDB TIMESTAMP values become Python datetime objects and DECIMAL values become Python Decimal objects). The following example calls the Results procedure, which returns a single table containing each contestant's name, number, and their total votes received:

response = client.call("Results")

if response.status != 1:
   print("Results failed:", response.statusString)
else:
   table = response.tables[0]
   for row in table.tuples:
      name, number, total = row
      print(f"Contestant {number}, {name}: {total} votes."

8.4.5. Closing the Connections to the Database Cluster

The client runs background threads for its connections, so it is important to close it when your application is done. This is done manually by calling close():

client = VoltClient(hosts=["voltsvr1", "voltsvr2"])

try:
   ... # make procedure calls
finally:
   client.close()

Alternatively, you can use the client as a context manager with the Python with ... as syntax, which closes the connection automatically:

with VoltClient(hosts=["voltsvr1", "voltsvr2"]) as client
   ... # make procedure calls

When you make asynchronous calls, it is important to ensure all calls complete before closing the connection. The following example uses concurrent.futures.wait, which only returns when every future has a result or error, to ensure the connection does not close prematurely:

try:
   futures = [client.call_async(...) for ... in ...]
finally:
   concurrent.futures.wait(futures)   # returns when every call has completed
   client.close() # safely closes the connection