---
title: "Python Server SDK"
description: "Evaluate configs and feature flags locally for any context with the Python server SDK, with updates delivered over server sent events"
canonical_url: "https://docs.configdirector.com/sdks/server/python"
---
# Python Server SDK

> Evaluate configs and feature flags locally for any context with the Python server SDK, with updates delivered over server sent events

<div hidden="true" dataTitleIcon="python">



</div>

<external-badges package-url="https://pypi.org/project/configdirector-server-sdk/" registry="pypi" repo="python-server-sdk/tree/main/configdirector-server-sdk">



</external-badges>

## Introduction

The Python Server SDK is a server-side SDK that evaluates configs and their targeting rules. Upon initialization, it retrieves configs and targeting rules from ConfigDirector services. From that point on, it evaluates configs locally for any context, and receives updates via server sent events (SSE) when configs are updated on the dashboard or via the admin API.

The minimum Python version supported is `3.10`.

## Installation

The SDK can be installed from PyPI: [https://pypi.org/project/configdirector-server-sdk/](https://pypi.org/project/configdirector-server-sdk/)

<code-group>

```bash [pip]
pip install configdirector-server-sdk
```

</code-group>

## Configure and initialize the client

1. Create an instance of the client providing your server SDK key. You can retrieve a server SDK key for each environment under `SDK keys` in the dashboard's navigation panel.
2. Initialize the client to initiate its connection lifecycle.

```python [config_director_setup.py]
from configdirector import create_client

client = create_client("YOUR-SERVER-SDK-KEY")

# Blocks until the client is initialized or it times out.
# If initialization times out, the client will continue attempting to
# initialize in the background.
client.initialize()
```

<warning>

Server SDK keys are secret values. Do not commit them to your source code repository. Provide them at runtime via environment variables instead.

</warning>

### Additional configuration options

#### `metadata`

The `metadata` option allows you to provide your application's name and version. These values can be used in targeting rules conditionals. For example, if a certain feature should only be enabled starting with a certain version of your application.

```python [config_director_setup.py]
from configdirector import Metadata, create_client

client = create_client(
    "YOUR-SERVER-SDK-KEY",
    metadata=Metadata(app_name="YOUR-APP-NAME", app_version="1.0.2"),
)

client.initialize()
```

#### `log_level` and `logger`

The SDK logs through the standard library to a logger named `configdirector`. Logging levels, formatting, and destinations can be controlled by accessing the logger:

```python
import logging

logging.getLogger("configdirector").setLevel(logging.DEBUG)
```

The SDK sets no level of its own, so this logger follows the usual `logging` rules. With nothing
configured at all, Python still surfaces warnings and errors on `stderr`. If you would rather not configure `logging`, the `log_level` parameter can be used to set a level on the logger:

```python
client = create_client("YOUR-SERVER-SDK-KEY", log_level="DEBUG")
```

The logger can be overridden by passing any object implementing `ConfigDirectorLogger` (a
different stdlib logger, or your own adapter) in the `logger` parameter:

```python
client = create_client("YOUR-SERVER-SDK-KEY", logger=logging.getLogger("my_app.configdirector"))
```

#### `connection`

The connection parameter accepts a `ConnectionOptions` object with the following optional values:

- `mode`
  - The connection mode, one of `streaming`, `polling` or `offline`. It is recommended to use the default of `streaming` unless you have a specific need to use `polling` instead. In `offline` mode the client never connects to ConfigDirector and serves the values you give it; see [Run without a connection](#run-without-a-connection).
  - Defaults to `streaming`
- `file`
  - The path of a JSON file holding the values to serve in `offline` mode, as a `str` or a `pathlib.Path`, resolved against the process working directory. Only one of `file` and `values` can be given, and only in `offline` mode. See [Run without a connection](#run-without-a-connection).
- `values`
  - The values to serve in `offline` mode, keyed by config key. Only one of `file` and `values` can be given, and only in `offline` mode. See [Run without a connection](#run-without-a-connection).
- `polling_interval`
  - Only used in `polling` mode. The interval, in seconds, to poll ConfigDirector services for updates. A value below the minimum of `60` seconds is raised to `60` seconds and a warning is logged. See [Polling intervals](https://docs.configdirector.com/getting-started/architecture#polling-intervals).
  - Defaults to `5` minutes (`300` seconds)
- `timeout`
  - The timeout, in seconds, to be used in initialization. This is how long the `initialize` method will wait for data from ConfigDirector services. If the timeout is reached, `initialize` will return but the client will still be in an unready status and returning in-code default values. The client will continue to attempt to connect and retrieve config values in the background. It has no effect in `offline` mode.
  - Defaults to `3` seconds
- `url`
  - The base URL used to connect to ConfigDirector services
  - This should only be provided if your environment requires you to configure a proxy server in order to connect to ConfigDirector services. It has no effect in `offline` mode.

```python
from configdirector import ConnectionOptions, create_client

client = create_client(
    "YOUR-SERVER-SDK-KEY",
    connection=ConnectionOptions(mode="polling", polling_interval=600, timeout=5),
)
```

#### `telemetry`

Telemetry tuning. It is unlikely these settings need to be adjusted. However, in cases where your application has a large number of evaluations per second, you can adjust these settings to tune the memory footprint and frequency of telemetry requests.

Keep in mind that ConfigDirector relies on these telemetry events to provide insights and features related to the configs being used.

From version 1.9.0 of the SDK, each evaluation it reports says which variation was served and which percentage rollout and share served it. That is how ConfigDirector counts the contexts each variation of an experiment reached. [Tracked events](#track-events) are sent with the same reports.

The telemetry object accepts the following optional values:

- `event_queue_limit`
  - The size limit of telemetry event queues. If the size limit is reached before the events are flushed to the network, older events will be dropped.
  - ConfigDirector keeps a count of dropped events. If the number of dropped events is higher than 50% of the total events, ConfigDirector will issue an alert in the dashboard.
  - A number between 100 and 100,000. Defaults to 5,000.
- `flush_interval`
  - How often events are flushed and sent over the network in seconds.
  - Decrease this number if your application consistently captures a large number of events in short periods of time in order to reduce memory footprint from a large event queue.
  - Defaults to 30 seconds.

```python
from configdirector import TelemetryOptions, create_client

client = create_client(
    "YOUR-SERVER-SDK-KEY",
    telemetry=TelemetryOptions(event_queue_limit=10_000, flush_interval=15),
)
```

### Hooks

Hooks are events you can subscribe to in order to be notified of some key actions from the client. In order to subscribe to hooks you can provide a handler in the options, or use the `on` method which returns a `Subscription`:

```python
subscription = client.on("configs_updated", lambda event: print(event.keys))
```

```python
from configdirector import ClientHooks, create_client

client = create_client(
    "YOUR-SERVER-SDK-KEY",
    hooks=ClientHooks(config_evaluated=lambda event: print(event.evaluation)),
)
```

The following hooks are available:

- `client_ready: ClientReadyEvent`
  - Emitted when the client is initialized. When this event is emitted it means the client has received a payload from the ConfigDirector servers and is ready to evaluate configs.
- `configs_updated: ConfigsUpdatedEvent`
  - Emitted when a payload is received from the ConfigDirector servers with config data. It is emitted during initialization when an entire config payload is received. After initialization, it is emitted when updates are pushed from the server (or discovered via polling if that connection mode is used).
  - `keys: list[str]` - The config keys that were included in the payload from the server, and the keys of configs whose targeting rules use a segment the payload included, sorted.
  - `removed_keys: list[str]` - The keys of configs the payload removed, sorted. A config is removed when a full payload no longer includes it. Empty when nothing was removed.
- `config_evaluated: ConfigEvaluatedEvent`
  - Emitted whenever a config is evaluated. This includes calls to `get_value` and evaluations delivered to watchers via `watch`.
  - `evaluation: ConfigEvaluation` - A `ConfigEvaluation` object containing the details of the evaluation:
  
    - `key: str` - The config key that was evaluated
    - `value: str | int | float | bool | dict | list` - The value the config evaluated to. It can be the default value provided to `get_value` or `watch`. For example, if the config was evaluated before the client was initialized.
    - `value_id: str | None` - The value ID, a stable hash of the value that you can send to a third party rather than the `value`. This can be useful for large values, like a JSON config, or to avoid disclosing the values themselves. It may be `None` if the evaluation had to fall back to the in-code default value. To report an experiment to an analytics tool, send the variation instead: the pages for [Amplitude](https://docs.configdirector.com/integrations/analytics/amplitude), [Mixpanel](https://docs.configdirector.com/integrations/analytics/mixpanel), [Google Analytics 4](https://docs.configdirector.com/integrations/analytics/ga4), [Segment](https://docs.configdirector.com/integrations/analytics/segment) and [PostHog](https://docs.configdirector.com/integrations/analytics/posthog) name the event each one reads.
    - `variation: Variation | None` - The variation that was served, with its `id` and `name`. The `id` stays the same when the variation is renamed, and `name` is `None` for a variation without one. It is `None` when the config has no variations or the in-code default value is returned. Versions of the SDK before 1.9.0 do not report it.
    - `is_default: bool` - Whether or not the evaluation fell back to the default value provided in code.
    - `reason: EvaluationReason` - The reason for the evaluation resolution. In the case the config successfully evaluated based on server-provided targeting rules, it will be `found-match`. A context without an `id` that is served the default targeting rule value after skipping a percentage rollout reports `rollout-skipped-no-identifier`, with `is_default` set to `False`; see [Contexts without an `id`](#contexts-without-an-id). If the evaluation had to fall back to the default value, the `reason` will encode why the fallback was required:
    
      - `client-not-ready` - The evaluation happened before the client finished initialization
      - `config-state-missing` - The requested config key was not present in the payload received from the server. This could be due to an incorrect config key, or a config that was not enabled to be available to client SDKs.
      - `invalid-number` - The config was requested as a number but the value received from the server could not be converted to a number.
      - `invalid-boolean` - The config was requested as a boolean but the value received from the server could not be converted to a boolean.
      - `invalid-json` - The config was requested as a JSON object but the value received from the server could not be converted to a JSON object.
      - `type-mismatch` - The config was requested with a data type that did not match the type of the config and no reasonable type conversion was possible.
      - `value-missing` - The config key was present in the payload received from the server, but it carried no value.
    - `context: Context | None` - The context that was provided to the evaluation function, or `None` if no context was provided.

### Run without a connection

Set the connection `mode` to `offline` to run your application without connecting to ConfigDirector, for example in a suite of UI tests where each run pins every flag to a known value. An offline client needs no server SDK key, opens no connection and sends nothing, so the application is invisible to the ConfigDirector dashboard: no evaluations are counted and no lifecycle state changes. Every context receives the same value and targeting rules are not applied.

Give the values in a JSON file, with one value per config key, or inline:

```python [config_director_setup.py]
from configdirector import ConnectionOptions, create_client

client = create_client(
    connection=ConnectionOptions(mode="offline", file="./flags/new-checkout-on.json"),
)
client.initialize()
```

```json [flags/new-checkout-on.json]
{
  "new-checkout": true,
  "max-items": 20,
  "greeting": "hello",
  "theme": { "color": "blue" }
}
```

```python [config_director_setup.py]
from configdirector import ConnectionOptions, create_client

client = create_client(
    connection=ConnectionOptions(mode="offline", values={"new-checkout": True, "max-items": 20}),
)
client.initialize()
```

The file path is resolved against the process working directory. The config type of each value follows from the value itself: `true` or `false` (`boolean`), a whole number written without a decimal point or exponent (`integer`), any other number (`float`), a string, or an object or array (`json`). Inline values follow the same rule with Python types: a `bool`, an `int`, a `float`, a `str`, or a `dict` with `str` keys or a `list`. A value read with an in-code default value of another type returns the in-code default value, as it does against ConfigDirector. A watch registered before `initialize` is called with the offline value when `initialize` loads it. Offline values never change on their own, so the watch is called again only if `initialize` is called again, which loads the values, and reads the file, once more. `polling_interval`, `timeout`, `url` and the `timeout` argument of `initialize` have no effect.

With `mode="offline"` and neither `file` nor `values`, the client is ready at once and every config returns its in-code default value.

`initialize` raises a `ConfigDirectorInitializationError`, which the SDK exports, when the values cannot be loaded: the file is missing, cannot be read, is not a JSON object or holds a value such as `null`; `file` and `values` are both given; or either is given in `streaming` or `polling` mode. The client stays not ready, so a test run that starts from a misspelled path fails instead of passing on in-code default values. A later `initialize` that cannot load the values raises the same way and keeps serving the values loaded before.

If you come from LaunchDarkly, a file of values is what LaunchDarkly calls "flags from files", and `offline` with no values is its "offline mode".

## Retrieve config values

Retrieving config values via `get_value`, and subscribe to updates via `watch`. The first argument is the config key, the second is the default value. The type of the default value provided determines the type the config is parsed as. The default value is returned if the config is not available or cannot be parsed as the given type:

```python [main.py]
# Retrieve config values
retries = client.get_value("max-retries", 3)
theme = client.get_value("theme", "light")
limits = client.get_value("rate-limits", {"per_minute": 60}) # JSON config

# Watch for config updates
def on_change(value: bool) -> None:
    print(f"new-checkout is now {value}")

subscription = client.watch("new-checkout", False, on_change)

# Call close when the subscription is not longer needed
subscription.close()
```

### Evaluate config values with a context

<note>

Unlike client SDKs, the server SDKs are able to evaluate targeting rules for the given context locally without additional network calls.

</note>

`get_value` accepts three arguments, the first argument is the config key, the second one is the default value to be returned if the client is not yet initialized, and the third and optional argument is a context to be used for targeting rules evaluation:

```python [main.py]
from configdirector import Context

client.get_value("my-boolean-config-key", False, Context(id="user-id", name="Example User"))
```

To evaluate configs for an entity other than a user, such as a tenant, set the context kind with `kind`:

```python [main.py]
tenant = Context(id="acme", kind="tenant", name="Acme Inc.", traits={"plan": "enterprise"})

client.get_value("my-boolean-config-key", False, tenant)
```

Leaving `kind` out means `user`. A context kind is 1 to 64 lowercase letters, digits, dashes, or underscores. Contexts of different kinds are separate contexts even when they share an identifier, and a percentage rollout can place them in different shares. A context with an invalid context kind is ignored: the SDK logs an error, and configs are evaluated as if no context had been given. Context kinds are available from version 1.7.0 of the SDK.

`watch` accepts four arguments, the first is the config key, the second argument is the default value, the third argument is a function that will be executed when the config value is updated, and the fourth and optional argument is a context:

When a full payload no longer includes the watched config, the function is called with the default value, so it always agrees with what `get_value` would return.

```python [main.py]
def on_change(value: bool) -> None:
    print(f"new-checkout is now {value}")

subscription = client.watch(
  "new-checkout",
  False,
  on_change,
  Context(
    id="user-id",
    name="Example User",
    traits={"region": "North America"}
  )
)
```

#### Contexts without an `id`

A percentage rollout places each context in a share by its `id`, so a context without an `id` skips percentage rollouts: a rollout does not apply to it, and the targeting rules after the rollout are tried, as they are after a conditional rule the context does not match. The default targeting rule value is served when none of them applies. A rule that serves a single value still applies to it. To keep an anonymous visitor in a rollout, give the context a stable `id`, such as a session ID. Version 1.9.0 of the SDK ended the evaluation at the skipped rollout and served the default targeting rule value even when a later rule matched, and versions before 1.9.0 served every context without an `id` the first share of the rollout.

## Track events

`track` reports to ConfigDirector that something happened in your application, such as a completed checkout. ConfigDirector shows tracked events on the Live feed and counts them for each variation of a running experiment; see [Tracked events](https://docs.configdirector.com/monitoring/activity#tracked-events).

`track` accepts the name of the event and an optional context the event happened for. The event's `value` and `details` are optional keyword arguments:

```python [main.py]
from decimal import Decimal

from configdirector import Context

client.track(
    "checkout-completed",
    Context(id="user-id"),
    value=Decimal("49.90"),
    details={"plan": "pro", "seats": 3},
)

client.track("export-finished", Context(id="acme", kind="tenant"))

client.track("nightly-sync-finished")
```

- `value` is a number the event measures, such as an amount of money, a count or a duration. It can be an `int`, a `float` or a `Decimal`, and a `Decimal` is sent with every digit, so `Decimal("49.90")` arrives as `49.90`.
- `details` is a mapping describing the event. Its values are strings, numbers, booleans, `datetime` values, or nested mappings and lists. A `datetime` is sent as ISO 8601 text, such as `2026-12-01T00:00:00+00:00`, keeping its UTC offset if it has one.

An event counts for an experiment only when it is tracked for a context with an `id`, the same one the experiment's config was evaluated for, so pass the context you evaluate with when you track the outcome of an experiment. Without a context, or for a context without an `id`, the event is still reported and counts for no experiment. For a context marked `anonymous`, the event is reported without the `id`, so it counts for no experiment either. A context with an invalid context kind is ignored as it is in an evaluation: the SDK logs an error and reports the event without a context.

`track` returns nothing and never raises. The event is sent with the SDK's next telemetry report, every 30 seconds unless you changed [`flush_interval`](#telemetry). In [`offline` mode](#run-without-a-connection) and after `close`, `track` does nothing. The SDK applies these limits, and logs one warning for each of them per telemetry report:

<table>
<thead>
  <tr>
    <th>
      Limit
    </th>
    
    <th>
      Beyond it
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      An event name of 1 to 150 characters
    </td>
    
    <td>
      The event is not reported.
    </td>
  </tr>
  
  <tr>
    <td>
      1,000 tracked events in one telemetry report
    </td>
    
    <td>
      Further events are not reported until that report is sent.
    </td>
  </tr>
  
  <tr>
    <td>
      Details of up to 20 top-level keys and 1 KB as JSON, holding only the types of value above
    </td>
    
    <td>
      The details are left out and the event is still reported.
    </td>
  </tr>
  
  <tr>
    <td>
      A <code>
        value
      </code>
      
       that is a finite number, which a <code>
        bool
      </code>
      
       is not
    </td>
    
    <td>
      The value is left out and the event is still reported.
    </td>
  </tr>
</tbody>
</table>

ConfigDirector applies limits of its own to the events it stores; see [Limits](https://docs.configdirector.com/monitoring/activity#limits).

`track` is available from version 1.9.0 of the SDK, which exports the `TrackedEventDetails` and `TrackedEventDetailValue` types. A `datetime` inside `details` is sent from version 1.9.1; version 1.9.0 left the details out.

## Other useful client features

The client provides additional methods.

### `evaluate`

Evaluates a config the way `get_value` does, with the same three arguments, and returns the `ConfigEvaluation` behind it — the value, whether it is the in-code default value, the `reason`, the `value_id`, the `variation`, and the `context` — instead of the value alone. It is the same object the [`config_evaluated` hook](#hooks) reports, so use it where the reason matters at the point of the read, such as when reporting an evaluation to another system; `get_value` is enough everywhere else. Like every read, it is reported to `config_evaluated` and counted in telemetry. The SDK exports the `ConfigEvaluation`, `EvaluationReason` and `Variation` types.

```python [main.py]
evaluation = client.evaluate("my-config-key", False, Context(id="user-id"))
if evaluation.reason == "config-state-missing":
    print("No config with this key reached the SDK.")
```

`evaluate` is available from version 1.9.0 of the SDK.

### `get_all_configs`

Returns every config evaluated for a context, as a `dict` of config keys to `ConfigState`, for example to hand the values to a page you render on the server. Pass `config_keys` to get only those keys. It returns an empty `dict` until the client is ready, and these reads are not counted in telemetry.

```python [main.py]
configs = client.get_all_configs(Context(id="user-id"), config_keys=["new-checkout"])
```

Each `ConfigState` carries the config's `id`, `key` and `type`, the `value` served as text, its `value_id` and its `variation`. When a percentage rollout served the value, it also carries the rollout's `rule_id` and `share_id`. `variation`, `rule_id` and `share_id` are available from version 1.9.0 of the SDK.

## Test your code

Use the SDK's `configdirector.testing` module to test the code that reads your configs and flags. It creates a **test client**: the SDK's real client connected to an in-memory server that your test controls. Only the connection to ConfigDirector and the telemetry are replaced. Everything else is the same code that runs in production, so the code under test behaves exactly as it does against ConfigDirector, and no network connection is opened, no telemetry is sent, and no thread is started. The module ships in the package, so there is nothing else to install.

```python [test_checkout_service.py]
from configdirector.testing import create_test_client

with create_test_client({"new-checkout": True, "max-items": 20}) as test_client:
    service = CheckoutService(test_client.client)
    test_client.client.initialize()

    assert service.is_new_checkout_enabled("user-123")

    test_client.set_value("new-checkout", False)
    assert not service.is_new_checkout_enabled("user-123")
```

`test_client.client` is a `ConfigDirectorClient`, so it goes anywhere your code accepts one. It starts uninitialized, like a production client, because the code under test usually owns the call to `initialize`; with no hold or failure armed, `initialize` completes at once with the stored values. The test client is a context manager that closes its client on exit; unlike the client's own context manager, entering it does not initialize the client.

### Module-level clients

An application that creates and initializes its client when a module loads, as the [Flask sample](https://github.com/ConfigDirector/python-server-sdk/tree/main/samples/configdirector-server-sdk/flask) did, runs the real client before any test can intervene, which opens a connection and starts threads. A `monkeypatch` also has to reach every module that bound the name: `from configdirector_client import client` binds its own `client`, so patching `configdirector_client.client` does not change what the importing module sees. Structure the application around a lazy getter instead, and replace that in tests:

```python [configdirector_client.py]
import os
import threading

from configdirector import ConfigDirectorClient, create_client

_client: ConfigDirectorClient | None = None
_client_lock = threading.Lock()


def get_client() -> ConfigDirectorClient:
    global _client
    with _client_lock:
        if _client is None:
            _client = create_client(os.environ["CONFIGDIRECTOR_SERVER_SDK_KEY"])
            _client.initialize()
        return _client
```

The lock matters on a threaded server: without it, two first requests arriving together each build and initialize a client.

```python [test_app.py]
import pytest

import app
from configdirector.testing import create_test_client


def test_new_checkout(monkeypatch: pytest.MonkeyPatch) -> None:
    with create_test_client({"new-checkout": True}) as test_client:
        test_client.client.initialize()
        monkeypatch.setattr(app, "get_client", lambda: test_client.client)

        assert app.test_client().get("/checkout").json["new-checkout"] is True
```

Patch the name in the module that calls it (`app.get_client` when `app.py` does `from configdirector_client import get_client`), and initialize the test client where the application's getter would have.

### Values

`create_test_client`, `set_value`, and `replace_values` take native values, and the config type follows from each value's type: a `bool`; an `int` (an integer config); a `float` (a float config); a `str`; or a `dict` with `str` keys or a `list` (a JSON config). A `str` is always a string config, so JSON text meant as a JSON config is given as `json.loads(text)`. Every value is served as an unconditional config, so every context receives the same value; a test that needs different values per context is written as one test per value. `None`, an unsupported type such as a `tuple` or a `set`, or a `dict` with a non-`str` key raise `ConfigDirectorTypeError`; a blank key, a non-finite number, or an integer with too many digits to write out raise `ConfigDirectorValidationError`. In every case nothing changes.

Reads behave as they do in production: `set_value("k", True)` read as a string returns the in-code default value with the `type-mismatch` reason, `set_value("k", 2.5)` read with an `int` default returns the default with the `invalid-number` reason, and `set_value("k", "")` serves the in-code default value with the `value-missing` reason. Reasons arrive on the `config_evaluated` event. JSON objects keep their insertion order.

### Controls

<table>
<thead>
  <tr>
    <th>
      Control
    </th>
    
    <th>
      Effect
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        set_value(key, value)
      </code>
    </td>
    
    <td>
      Stores the value and, once the client is connected, delivers an update carrying only <code>
        key
      </code>
      
      . Watches of <code>
        key
      </code>
      
      , <code>
        configs_updated
      </code>
      
       handlers, and reads see it before the call returns.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        remove_value(key)
      </code>
    </td>
    
    <td>
      Removes the value and delivers a full update without it. Reads return the in-code default value with the <code>
        config-state-missing
      </code>
      
       reason, watches of <code>
        key
      </code>
      
       receive the default, and <code>
        configs_updated
      </code>
      
       lists <code>
        key
      </code>
      
       in <code>
        removed_keys
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        replace_values(values)
      </code>
    </td>
    
    <td>
      Replaces every stored value, disarms any armed hold or failure, and delivers a full update. Use it to reset a test client shared across tests.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        hold_initialization()
      </code>
    </td>
    
    <td>
      The next <code>
        initialize
      </code>
      
       waits until <code>
        complete_initialization()
      </code>
      
       or <code>
        fail_initialization()
      </code>
      
      , or until the client's timeout elapses. <code>
        initialize
      </code>
      
       blocks, so a test calls it on another thread.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        complete_initialization()
      </code>
    </td>
    
    <td>
      Delivers the stored values to the held <code>
        initialize
      </code>
      
      , on the calling thread, so the client is ready when the call returns. Called while a hold is armed but not picked up, it disarms the hold.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fail_initialization()
      </code>
    </td>
    
    <td>
      Fails the held <code>
        initialize
      </code>
      
       the way an invalid SDK key does: it completes promptly, the client is not ready, and the error is logged. Called with no <code>
        initialize
      </code>
      
       held, it arms the next one to fail.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        tracked_events
      </code>
    </td>
    
    <td>
      The events the code under test tracked with <code>
        track
      </code>
      
      , in call order, each as a <code>
        TrackedEvent(name, context, value, details)
      </code>
      
      , which <code>
        configdirector.testing
      </code>
      
       exports. An event whose name the SDK would not report is not recorded, and a value or details the SDK would leave out are missing from the record. The limit of 1,000 events per telemetry report does not apply to the record.
    </td>
  </tr>
</tbody>
</table>

After a failed attempt, the next `initialize` succeeds and delivers the values stored in the meantime.

```python [test_checkout_service.py]
with create_test_client({"new-checkout": True}, timeout=0.5) as test_client:
    test_client.fail_initialization()

    test_client.client.initialize()

    assert not test_client.client.is_ready
    assert not service.is_new_checkout_enabled("user-123")
```

### Options

`create_test_client(values, timeout=..., logger=...)` takes `timeout` (the client's connection timeout in seconds, which bounds how long a held `initialize` waits; defaults to the SDK's production timeout, and a timeout given to `initialize` itself takes precedence) and `logger` (defaults to the standard library logger named `configdirector`, so pytest's `caplog` captures it).

### What to expect

- The first update is delivered while `initialize` connects, so watches registered before `initialize` run before it returns, and `configs_updated` fires before `client_ready`.
- Watches and handlers run on the thread that calls `set_value`, `remove_value`, `replace_values`, `complete_initialization`, or `initialize`, not on a transport thread.
- A held `initialize` that times out, or that is interrupted by `close`, leaves the client not ready until the next `initialize`, and logs the SDK's timeout warning; production streaming would keep retrying in the background. Readiness is sticky: once the client is ready, a later held, failed, or timed-out attempt leaves it ready with its previous values, and completing such an attempt fires no `client_ready`.
- Watches fire on every update carrying their key, whether or not the value changed, as in production. `remove_value` and `replace_values` deliver a full update, so they also fire the watches of every remaining key.
- An exception raised by a watch or handler is logged and does not propagate, as with the production transports.
- After `close`, `is_ready` is false, `get_value` serves the last values, `get_all_configs` returns an empty dict, `initialize` raises `ConfigDirectorValidationError`, every watch and event handler has been removed, the test client's controls are silent no-ops, and no SDK thread is left running.


## Sitemap

See the full [sitemap](https://docs.configdirector.com/sitemap.md) for all pages.
