> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vexlo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK

> Install the SDK and instrument entry points, helpers and concurrent work.

Initialize the SDK at application startup, instrument the functions to record, and connect
the application's local queue to a relay. A trace begins at an instrumented entry point;
recorded helpers contribute their inputs, return values, errors and call relationships.

## Requirements and installation

Use Python 3.10 or newer and Git. Install the SDK into the environment that runs your service.

For a new application environment, run these commands from the application directory:

```sh theme={"system"}
python3.14 -m venv .venv
.venv/bin/python -m pip install 'git+https://github.com/vexlo-ai/vexlo-sdk-python.git'
```

The commands use Python 3.14. Use your supported interpreter name if different. If the service
already has a virtual environment, run the installation with that environment's Python
instead of creating another one. Configure the service runner to use that interpreter.

## Initialize the application

Run the [server and relay](https://docs.vexlo.ai/deployment/install.md) before checking delivery. Set
`VEXLO_SPOOL_DIR` in the application's environment to the absolute queue directory used by
the relay. If you used the local installation procedure, load its saved `vexlo-env.sh` file
to restore the path. For another deployment, use the relay's configured absolute queue path.

In the shell that runs the example, replace this path with that directory:

```sh theme={"system"}
export VEXLO_SPOOL_DIR="/absolute/path/to/vexlo-spool"
```

The application and relay must access the same queue directory on the same filesystem.
Run them on the same host, or mount that directory into both containers. Matching path strings
on different machines do not share a queue. Both processes need read and write access;
use the absolute path even when their working directories differ.

The SDK writes locally and does not take an endpoint or ingest token. The relay forwards the
records; readers connect to the server with a read token.

By default, marked calls capture arguments, return values and exception details. Before
applying the markers to real requests, choose the settings in
[Capture controls](https://docs.vexlo.ai/instrumentation/capture.md). Field-name rules do not filter credentials
embedded in formatted messages.

Save this as `checkout.py` to verify the integration. In an existing service, initialize at
startup and apply the decorators to its handler and helpers.

```python theme={"system"}
import json
import os
from uuid import uuid4

import vexlo

vexlo.init(
    service="checkout",
    spool_dir=os.environ["VEXLO_SPOOL_DIR"],
    tag_keys=["order_id"],
)


@vexlo.step
def lookup_inventory(sku):
    return {"sku": sku, "available": 0}


@vexlo.step
def can_fulfill(inventory, quantity):
    return inventory["available"] >= quantity


@vexlo.trace
def checkout(order_id, sku, quantity):
    inventory = lookup_inventory(sku)
    return {"order_id": order_id, "accepted": can_fulfill(inventory, quantity)}


print(json.dumps(checkout("order-" + uuid4().hex[:8], "sku-7", 1)))
vexlo.flush()
```

`service` identifies the application in recordings. `tag_keys` names entry-point arguments to
harvest for search; it does not harvest that argument from every helper. The explicit queue
path keeps initialization independent of the process's home directory.

Initialize once before handling requests. Reinitialization replaces the writer and instance
identity; doing so during recorded work can leave calls without completion data. See
[Python API](https://docs.vexlo.ai/reference/python.md#reinitialization) for configuration and lifecycle boundaries.

## Select entry points and helpers

`@vexlo.trace` starts a trace only when none is active. Inside an existing trace it behaves
as a step. `@vexlo.step` records within an active trace; without one, the function executes
without creating a recording. Instrumenting helpers alone therefore produces no trace.

The graph connects each call to its nearest recorded parent. An uninstrumented function
between two recorded calls does not appear as an intermediate node. Select the boundaries
needed to explain the operation, rather than treating the graph as a complete source call graph.

Both markers accept `retries=True` for known retries. Consecutive recorded calls of the same
function after a failure can then share one step, including a successful recovery with changed
arguments. See [retry grouping](https://docs.vexlo.ai/reference/python.md#retry-grouping) for inferred matching and
its limits.

## Add searchable identifiers

Declare known entry-point arguments with `tag_keys`, as the example does for `order_id`.
For a runtime identifier, call `vexlo.tag(region="west")` inside the active entry point.
Tags are indexed in clear text; choose identifiers suitable for that use.

Runtime tags reach the opening record only before the background writer serializes it.
Late tags in a long-running function can be lost without a counter. Prefer declared argument
tags for identifiers that must be present. A name declared in both `tag_keys` and
`redact_keys` is rejected at initialization; runtime pairs under redacted names are dropped
and counted.

For code-change investigations, provide your application revision as a declared argument tag
or set it early enough to be recorded. `sdk_version` identifies the Vexlo SDK, not the
application source revision. Vexlo does not automatically establish that a recording matches
the current checkout.

## Async functions

The same markers support `async def` functions and preserve their coroutine-function shape
for framework dispatch. Capture begins when the coroutine first runs, not when it is created.
A coroutine that is never awaited produces no record. Context follows `await`, with separate
context for concurrent asyncio tasks.

Save and run `async_example.py`:

```python theme={"system"}
import asyncio
import os

import vexlo

vexlo.init(service="async-example", spool_dir=os.environ["VEXLO_SPOOL_DIR"])


@vexlo.step
async def normalize(name):
    await asyncio.sleep(0)
    return name.strip().lower()


@vexlo.trace
async def handle(name):
    return await normalize(name)


print(asyncio.run(handle("  Ada  ")))
vexlo.flush()
print(vexlo.diagnostics())
```

```sh theme={"system"}
.venv/bin/python async_example.py
```

This example returns `ada`. Check `diagnostics()` for `internal_errors`, `adapter_errors` and
`steps_outside_trace`; all were zero in this run. These counters describe capture inside the
process. Verify server delivery separately.

## Generators and early exit

A recorded generator or async generator spans its first resumption through exhaustion,
close or error. Its duration includes the consumer's time between resumptions. Yielded
items and generator return values are not captured as an output.

Close an async generator inside the trace if iteration ends early. Save this example as
`generator_example.py`; `aclosing()` closes it before the entry point returns:

```python theme={"system"}
import asyncio
import os
from contextlib import aclosing

import vexlo

vexlo.init(service="stream-example", spool_dir=os.environ["VEXLO_SPOOL_DIR"])


@vexlo.step
async def records():
    yield "first"
    yield "second"


@vexlo.trace
async def read_first():
    async with aclosing(records()) as stream:
        async for item in stream:
            return item


print(asyncio.run(read_first()))
vexlo.flush()
print(vexlo.diagnostics())
```

```sh theme={"system"}
.venv/bin/python generator_example.py
```

This example returns `first` with `drives_lost_to_trace_end: 0`. Without closing or
exhausting the generator inside the trace, later finalization can leave its recorded call
without a reported completion. That increments `drives_lost_to_trace_end`.

## Carry context into threads

Thread pools and bare threads do not inherit the active trace. Take a `Carrier` snapshot
before submitting work, then restore it within each worker using `attach()`. Save this example
as `threads.py`:

```python theme={"system"}
import os
from concurrent.futures import ThreadPoolExecutor

import vexlo

vexlo.init(service="ratings", spool_dir=os.environ["VEXLO_SPOOL_DIR"])


@vexlo.step
def score(candidate):
    return candidate["rating"]


@vexlo.trace
def rank(candidates):
    context = vexlo.carrier()

    def scored(candidate):
        with vexlo.attach(context):
            return score(candidate)

    with ThreadPoolExecutor(max_workers=2) as pool:
        return sorted(pool.map(scored, candidates), reverse=True)


print(rank([{"rating": 4.8}, {"rating": 4.2}]))
vexlo.flush()
```

```sh theme={"system"}
.venv/bin/python threads.py
```

```text theme={"system"}
[4.8, 4.2]
```

The executor finishes its work before `rank` returns, so both `score` calls complete inside
the trace. Join or await child work before the entry point ends if you need its recorded
completion. Calls entered after trace closure record nothing; calls finishing afterward can
leave an open step with no result. Diagnostics distinguish those cases.

A carrier is an in-process snapshot. For separate processes or services, use shared tags to
find related traces; do not treat a carrier as a network propagation format.

## Verify instrumentation

Run the standalone example and retain its unique request identifier:

```sh theme={"system"}
.venv/bin/python checkout.py | tee checkout-result.json
```

Use the printed `order_id` in the UI's home search, open its trace and check that the
`checkout` root has both helpers. Select `lookup_inventory`, open **Output** and choose
**fetch full value**. Confirm that the returned inventory contains `available: 0`.

Alternatively, set `VEXLO_ENDPOINT` to your server URL
and `VEXLO_TOKEN` to a read token, then query the same identifier:

```sh theme={"system"}
ORDER_ID=$(.venv/bin/python -c \
  'import json,sys; print(json.load(sys.stdin)["order_id"])' < checkout-result.json)

curl --silent --show-error --fail-with-body --get "$VEXLO_ENDPOINT/v1/traces" \
  --data-urlencode service=checkout --data-urlencode tag_key=order_id \
  --data-urlencode "tag_value=$ORDER_ID" --data-urlencode limit=1 \
  -H "Authorization: Bearer $VEXLO_TOKEN"
```

Repeat the search if forwarding has not completed. For this example, confirm three steps
and the new request's timestamp. A search result confirms the summary, not readable values.
Continue with [Quickstart's HTTP verification](https://docs.vexlo.ai/quickstart.md#verify-through-http) to fetch the
graph and the inventory output using the same `checkout-result.json` file.

Inside the application, inspect `vexlo.diagnostics()` for
capture faults or calls outside a trace. Do not require every counter to be zero: intentionally
withheld tags and child work that outlives a trace have their own non-fault counters.

Application success, a local drain and server delivery are separate checks. `flush()` requests
a local drain with a nominal deadline; disk I/O can exceed it, and the call returns no
delivery acknowledgment. A server-side trace with the
expected helpers and readable values establishes the complete path for that execution.
Use [Troubleshooting](https://docs.vexlo.ai/deployment/troubleshooting.md) if any boundary fails.

Configure [Capture controls](https://docs.vexlo.ai/instrumentation/capture.md) before recording sensitive data.
Use [Python API](https://docs.vexlo.ai/reference/python.md) for exact signatures, configuration, limits and diagnostics.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.