Getting started

Create a client

Use the ready made envless client, configure it once with init, or build your own Envless. Every option, retries, timeouts, your own httpx client, and the raw escape hatch.

Set ENVLESS_TOKEN and the ready made client works anywhere, with no setup code:

python
from envless import envless

workspace = envless.workspace.get()

envless is a live handle on the default client. Importing it reads nothing, and the client is built from ENVLESS_TOKEN and ENVLESS_API_ENDPOINT the first time you touch a namespace on it, so import order between your entrypoint and your modules does not matter.

It is a proxy rather than an Envless, so isinstance(envless, Envless) is False and with envless: is not available. When you need the object itself, to type-check against it, close it or hand it to rotate_workspace_passphrase, call get_client().

To configure it in code instead, call init once at startup. Every module that imports envless then uses that client.

python
import os

from envless import init

init(os.environ['ENVLESS_TOKEN'], timeout=10, max_retries=5)

Three ways to build one

They differ only in who holds the instance and whether it is synchronous. Mix them freely; they do not interfere.

Use when
init(...)One workspace per process. The usual case. Installs the client envless resolves to, and returns it.
Envless(...)You want an explicit instance to pass around, or several clients at once. create_client is the same class under the TypeScript name.
AsyncEnvless(...)Your code runs on asyncio or trio. Same methods, awaited. See Async.
from envless import envless, init

init()

envless.products.list()

All three fall back to ENVLESS_TOKEN and ENVLESS_API_ENDPOINT whenever you leave token or base_url out, and raise ValueError with An Envless API key is required. Pass token= or set ENVLESS_TOKEN. Create one in the dashboard under Keys. when no token can be found.

Working with more than one workspace

A key is bound to one workspace, so reaching two means two clients. init holds exactly one, so build the others with Envless.

python
import os

from envless import Envless, envless

staging = Envless(os.environ['ENVLESS_STAGING_TOKEN'])

live = envless.variables.list_all('api', 'production')
preview = staging.variables.list_all('api', 'production')

The default client is shared by everything in the process. In a multi-tenant server that handles requests for different workspaces, do not init per request: build one Envless per tenant and pass it down.

The default client

ExportSignaturePurpose
envlessa live handleResolves to the installed client, building one from the environment on first use.
init(token=None, *, base_url=None, ...) -> EnvlessBuilds a client from the same options as Envless, installs it as the default, and returns it.
get_client() -> EnvlessReturns the real default instance, building it from the environment if init was never called.
reset_client() -> NoneDrops the installed client. The next access resolves afresh. Intended for tests.

Replacing the default with a second init does not close the first client, the same as in TypeScript. Close it yourself if you hold a reference to it.

Options

Envless, AsyncEnvless and init take the same arguments. The token is positional, the rest are keyword only.

OptionDefaultPurpose
tokenENVLESS_TOKENYour API key. Required.
base_urlENVLESS_API_ENDPOINT, then https://api.envless.cloudPoint at a self-hosted or local instance. Trailing slashes are trimmed. A bare host gets https://, except localhost, 127.0.0.1 and 0.0.0.0, which get http://, so localhost:2222 works as written. Write the scheme out for an IPv6 loopback, http://[::1]:2222.
max_retries3Retries idempotent requests on a transient status. It counts retries and not attempts, so the default is four requests at most.
timeout30.0Seconds each attempt may take, the response body included. 0 turns the limit off. Every method also takes timeout= for one call.
http_clienta new httpx.ClientBring your own httpx.Client, or httpx.AsyncClient for AsyncEnvless, for a proxy, custom TLS, instrumentation or tests.
user_agentenvless-sdk-python/<version>Sent as User-Agent. Set something identifiable for your own tooling.
disable_update_noticeFalseSilences the one-time check for a newer version on PyPI. ENVLESS_DISABLE_UPDATE_NOTICE does the same.
python
import os

from envless import Envless

envless = Envless(
    os.environ['ENVLESS_TOKEN'],
    base_url='http://localhost:2222',
    max_retries=5,
    timeout=60,
    user_agent='acme-provisioner/2.1',
)

Retries

The client retries only when retrying is safe. Two conditions must both hold:

The method is idempotent

GET, HEAD, PUT and DELETE are retried. POST and PATCH never are, because a create or an update that timed out may well have landed.

The status is transient

408, 429, 500, 502, 503 and 504. Everything else, including every 4xx you can fix, fails on the first attempt.

Backoff doubles off a 250ms base, so the first retry waits 500ms, then 1s, then 2s, and every delay is capped at 30 seconds. A Retry-After header is read as a number of seconds and replaces the computed delay whenever it parses above zero, under that same 30 second cap. A date-form Retry-After does not parse and is ignored.

A request that produced no response at all, a refused connection or a DNS failure, raises EnvlessNetworkError on the spot. There is no status to classify, so it is never retried.

Timeouts and cancellation

timeout is a hard deadline for each attempt, covering the connection, the headers and the whole response body, so a server that trickles a body cannot hold a call open past it. When it passes, the call raises EnvlessNetworkError with is_timeout set to True. Retries each get a fresh deadline.

python
from envless import EnvlessNetworkError, envless

try:
    products = envless.products.list_all(timeout=5)
except EnvlessNetworkError as error:
    if not error.is_timeout:
        raise

    print('The API did not answer within 5 seconds')

On Envless, each attempt with a deadline runs on a short-lived worker thread, which is what lets the deadline hold while a slow body trickles in. Two things follow. Event hooks, auth and transports on an http_client you pass run on that thread, without the caller's contextvars, so tracing or logging context set in the calling thread does not reach them. And an attempt that times out is abandoned, not interrupted: it finishes in the background before its connection goes back to the pool. timeout=0 keeps every attempt on the calling thread, with no deadline.

There is no cancellation argument, because Python has a better tool for it on the async client: cancel the task. asyncio.wait_for, asyncio.timeout on Python 3.11 and newer, anyio.fail_after and trio's cancel scopes all stop a call cleanly, connection included. See Async.

Bring your own httpx client

http_client takes any httpx.Client, which makes the whole SDK testable with no network and no mocking library: httpx.MockTransport answers every request in process.

python
import httpx

from envless import Envless


def handler(request: httpx.Request) -> httpx.Response:
    data: dict[str, object] = {
        'products': [],
        'pagination': {'limit': 25, 'offset': 0, 'totalCount': 0, 'hasMore': False},
    }

    return httpx.Response(200, json={'success': True, 'code': 200, 'message': 'OK', 'error': None, 'data': data})


envless = Envless(
    'ev_sk_test',
    base_url='https://api.test',
    http_client=httpx.Client(transport=httpx.MockTransport(handler)),
    disable_update_notice=True,
)

envless.products.list()

A bare list() sends no query string at all: limit, offset, search and order are only appended when you pass them, and the API applies its own default of 25. Ask for list(limit=25) and the call becomes GET https://api.test/products?limit=25.

It is also the seam for a proxy, a corporate CA bundle, event hooks that log every request, or respx if you already use it. Redirects are followed whatever the client you pass says, so a moved endpoint behaves the same as with the default, and the SDK's timeout replaces the client's own on every request. Envless takes an httpx.Client and AsyncEnvless an httpx.AsyncClient; the wrong kind raises TypeError.

The SDK closes only the httpx client it created. One you pass in stays open when the SDK client closes, because it is yours: close it when you are done with it.

The raw escape hatch

envless.raw is the underlying transport. It unwraps the response envelope, applies the same retry policy, and raises the same typed errors, so you keep every guarantee while reaching something the namespaces do not cover yet.

python
from envless import envless

payload = envless.raw.request('/products', query={'limit': 5, 'order': 'desc'})
ArgumentTypeNotes
pathstrAppended to base_url. Encode your own path segments.
methodstrDefaults to GET.
queryMapping[str, str | int | float | bool | None]None and '' are dropped rather than sent empty, and booleans go out as true and false.
bodyAnySerialised as JSON, and sets Content-Type.
timeoutfloatOverrides the client's timeout for this call.

raw.request returns the data field of the envelope, not the envelope. There is no way to read success, message or _req.reqId on a successful response, by design: a non-success envelope becomes an EnvlessApiError that carries the request id for you.

Reuse one client

A client holds one httpx connection pool and no cursor or per-request state, so build one per process and share it. Envless is safe to share between threads. Close it with close() or a with block when you are done, and AsyncEnvless with await aclose() or async with. The default envless client lives for the whole process.