Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/traceloop-sdk/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from traceloop.sdk.client import Client
from traceloop.sdk.client.http import HTTPClient
from traceloop.sdk.annotation.user_feedback import UserFeedback
from traceloop.sdk.datasets.datasets import Datasets
from traceloop.sdk.experiment import Experiment


def test_client_initialization():
Expand Down Expand Up @@ -46,3 +48,38 @@ def test_user_feedback_initialization():
assert isinstance(client.user_feedback, UserFeedback)
assert client.user_feedback._http == client._http
assert client.user_feedback._app_name == client.app_name


def test_client_lazy_loads_datasets():
"""Test client.datasets is only initialized lazy."""
client = Client(api_key="test-key", app_name="test-app")
assert client._datasets is None # Initial state is None

datasets = client.datasets

assert isinstance(datasets, Datasets)
assert client._datasets is not None # Then it's loaded
assert client.datasets is datasets # And always returns same instance


def test_datasets_deprecation_warnings():
"""Test client.datasets emits proper deprecation warnings."""
client = Client(api_key="test-key", app_name="test-app")
with pytest.deprecated_call():
client.datasets
with pytest.deprecated_call():
client.datasets = Datasets(client._http)
with pytest.deprecated_call():
del client.datasets

def test_client_lazy_loads_experiment():
"""Test cilent.experiment is only initialized lazy."""
client = Client(api_key="test-key", app_name="test-app")
assert client._experiment is None # Initial state is None

experiment = client.experiment

assert isinstance(experiment, Experiment)
assert client._experiment is not None # Then it's loaded
assert client.experiment is experiment # And always returns same instance

47 changes: 40 additions & 7 deletions packages/traceloop-sdk/traceloop/sdk/client/client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import sys
import os
from deprecated import deprecated

from traceloop.sdk.annotation.user_feedback import UserFeedback
from traceloop.sdk.datasets.datasets import Datasets
from traceloop.sdk.experiment.experiment import Experiment
from traceloop.sdk.client.http import HTTPClient
from traceloop.sdk.version import __version__
from traceloop.sdk.associations.associations import Associations
Expand All @@ -25,8 +24,6 @@ class Client:
api_endpoint: str
api_key: str
user_feedback: UserFeedback
datasets: Datasets
experiment: Experiment
associations: Associations
guardrails: Guardrails
_http: HTTPClient
Expand Down Expand Up @@ -65,9 +62,45 @@ def __init__(
timeout=httpx.Timeout(120.0),
)
self.user_feedback = UserFeedback(self._http, self.app_name)
self.datasets = Datasets(self._http)
experiment_slug = os.getenv("TRACELOOP_EXP_SLUG")
self._datasets = None
self._experiment = None
# TODO: Fix type - Experiment constructor should accept Optional[str]
self.experiment = Experiment(self._http, self._async_http, experiment_slug) # type: ignore[arg-type]
self.associations = Associations()
self.guardrails = Guardrails(self._async_http)

@property
def experiment(self):
# Lazy load only if accessed.
if self._experiment is None:
from traceloop.sdk.experiment.experiment import Experiment
experiment_slug = os.getenv("TRACELOOP_EXP_SLUG")
self._experiment = Experiment(self._http, self._async_http, experiment_slug) # type: ignore[arg-type]
return self._experiment

@property
@deprecated(
reason="datasets as client attribute is deprecated. Use a dedicated instance "
"of Datasets from traceloop.sdk.datasets"
)
def datasets(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you pls explain why you need this change ?

Copy link
Author

@tassadarius tassadarius Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As laid out already in my initial PR description (see section Reasoning)

You let everybody who has pandas installed pay a 35 MB tax, when loading this library, where it is not clear why the experimental and datasets should be loaded alongside the main client.

If it is experimental code, it should not be loaded automatically into the environment of consumers of the library.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed explanation @tassadarius , this is a valid concern.
We're currently working on some architectural changes to the SDK, and as part of that effort, we're planning to introduce package extras to properly address this.
This is on our roadmap but will take some time to implement properly without breaking existing users.
In the meantime, if this is blocking you, you could apply a local patch to defer the import — for example, by setting an environment variable check before the pandas-dependent imports in your local installation.

# Lazy load only if accessed.
if self._datasets is None:
from traceloop.sdk.datasets.datasets import Datasets
self._datasets = Datasets(self._http)
return self._datasets

@datasets.setter
@deprecated(
reason="datasets as client attribute is deprecated. Use a dedicated instance "
"of Datasets from traceloop.sdk.datasets"
)
def datasets(self, datasets: "Datasets"):
self._datasets = datasets

@datasets.deleter
@deprecated(
reason="datasets as client attribute is deprecated. Use a dedicated instance "
"of Datasets from traceloop.sdk.datasets"
)
def datasets(self):
self._datasets = None