Skip to content

feat(client): add __repr__ to QdrantClient, AsyncQdrantClient, QdrantRemote, AsyncQdrantRemote, QdrantLocal, AsyncQdrantLocal #1287

Description

@Harsh23Kashyap

feat(client): add __repr__ to QdrantClient, AsyncQdrantClient, QdrantRemote, AsyncQdrantRemote, QdrantLocal, AsyncQdrantLocal

Problem

None of the six client classes implements __repr__. The default Python repr() falls back to <qdrant_client.qdrant_client.QdrantClient object at 0x10c4a8eb0>, which is useless for debugging, logging, and Jupyter inspection.

httpx.Client, redis.Redis, boto3.client, and motor.AsyncIOMotorClient all ship with a meaningful __repr__. Verified: grep -n "__repr__\|__str__" qdrant_client/{qdrant_client,async_qdrant_client,qdrant_remote,async_qdrant_remote}.py qdrant_client/local/{qdrant_local,async_qdrant_local}.py returns nothing. No open or closed issue mentions the gap; checked via gh search issues "client repr".

Motivation

Three concrete user-facing problems this addresses:

  1. Debugging. When a long-running process logs an exception, the offending QdrantClient instance is shown as <qdrant_client.qdrant_client.QdrantClient object at 0x...>. The user has to know to call repr(client._client) or check init_options to see the host/path/mode.
  2. Jupyter. client in a notebook cell prints the memory address. Users coming from httpx.Client or boto3.client expect to see connection info at a glance.
  3. Log aggregation. Structured logging libraries (e.g. structlog, loguru) use repr() for objects by default. A meaningful repr makes log search and incident triage possible.

Background

The facade classes (QdrantClient, AsyncQdrantClient) hold an inner _client that is either a QdrantRemote/AsyncQdrantRemote or a QdrantLocal/AsyncQdrantLocal. The mode and the relevant connection info (host, port, gRPC vs REST, path) are already on these inner objects — __repr__ just needs to format and expose them.

QdrantRemote.__init__ stores _host, _grpc_port, _prefer_grpc, _scheme, _https. QdrantLocal.__init__ stores location and persistent.

The async facade and async remote are generated by the AST transformer pipeline at tools/async_client_generator/. The transformer converts defasync def for methods in async_methods (built from iscoroutinefunction(AsyncQdrantBase)). __repr__ is not in AsyncQdrantBase, so it stays sync in the async mirror — which is correct, because repr() is called synchronously by Python's built-in formatter.

Current behavior

>>> client = QdrantClient("localhost", port=6333, prefer_grpc=True)
>>> client
<qdrant_client.qdrant_client.QdrantClient object at 0x10c4a8eb0>
>>> client = QdrantClient(":memory:")
>>> client
<qdrant_client.qdrant_client.QdrantClient object at 0x10c4a8eb0>

Expected behavior

>>> QdrantClient("localhost", port=6333, prefer_grpc=True)
<QdrantClient mode=remote host='localhost:6333' prefer_grpc=True>
>>> QdrantClient(":memory:")
<QdrantClient mode=local location=':memory:'>
>>> QdrantClient(path="./storage")
<QdrantClient mode=local location='./storage'>
>>> QdrantRemote("https://api.qdrant.io", api_key="***")
<QdrantRemote scheme=https host='api.qdrant.io:443' prefer_grpc=False>
>>> QdrantLocal("/tmp/qdrant")
<QdrantLocal location='/tmp/qdrant'>
>>> QdrantLocal(":memory:")
<QdrantLocal location=':memory:'>

Async versions are identical (no Async prefix on the repr — the class name itself changes, and repr() is sync anyway).

The api_key is never included in the repr (security — secrets don't belong in reprs).

Proposed solution

Add a __repr__ method to each of the six classes:

  • QdrantClient — formats the inner _client repr with a mode= prefix
  • AsyncQdrantClient — same, but on the async class
  • QdrantRemote — formats host/port/scheme/prefer_grpc
  • AsyncQdrantRemote — same, but on the async class
  • QdrantLocal — formats location
  • AsyncQdrantLocal — same, but on the async class

Each __repr__ is a single method (3-6 lines). No new abstractions, no new state, no public API surface change beyond the special-method names.

For the generated async files (async_qdrant_client.py, async_qdrant_remote.py, local/async_qdrant_local.py), the AST transformer leaves __repr__ sync in the async mirror (since it's not in AsyncQdrantBase and not in async_methods). That is the correct behavior — repr() is always called synchronously. No exclude_methods or regen-script sed step needed.

Alternatives considered

  • Use dataclasses or __slots__ to auto-generate __repr__: rejected. The QdrantRemote repr needs derived fields (scheme, host:port formatting) that don't map cleanly to dataclass fields.
  • Only add __repr__ to the facade classes (QdrantClient/AsyncQdrantClient): rejected. Users who construct a QdrantRemote directly (rare but supported) and users in tests who want to inspect the inner client get nothing useful.
  • Use pprint for the repr: rejected. The default __repr__ returns a string, not a stream. pprint.pformat(...) adds visual noise for one-liners; not idiomatic for a short class summary.
  • Include api_key in the repr with masking: rejected. Secrets in reprs are a footgun (logs, exception tracebacks, debuggers all call repr). Better to omit entirely.

Scope

In scope:

  • 6 small __repr__ methods, one per class.
  • 1 test file with ~12 tests (sync + async × 3 classes × multiple configs).
  • Knowledge graph + repo_map updates.

Out of scope:

Acceptance criteria

  • repr(QdrantClient("localhost:6333", prefer_grpc=True)) returns a string that includes the host and the gRPC preference.
  • repr(QdrantClient(":memory:")) returns a string that includes :memory:.
  • repr(QdrantClient(path="./storage")) returns a string that includes the path.
  • The repr for the async client matches the sync client semantically (same fields, just different class name).
  • The repr never includes api_key or any other secret.
  • 12+ tests in tests/test_repr.py, all pass alongside the existing suite.
  • mypy clean on all 6 modified files.
  • ruff check + format clean on the new test file.
  • Running bash tools/generate_async_client.sh regenerates the async files with the __repr__ methods intact (no regen script changes needed).

Backward compatibility

Fully additive. The new __repr__ is a special-method name that doesn't conflict with anything. The default object.__repr__ is replaced only by the new method, which produces a more useful string.

Risks

  • Minimal. The repr is purely a debug/UX concern; it doesn't affect behavior, public API, or async semantics.
  • Low risk of name collision: __repr__ is a reserved special-method name. User code cannot override it on the client instances.

Notes for the implementer

  • The base branch for this work is upstream/dev (not master). Maintainer joein closed docs(client): correct gRPC default timeout in QdrantClient docstring #1269 on 2026-07-21 with "All the PRs should point dev branch, not master", and the PR template at .github/PULL_REQUEST_TEMPLATE.md:4 says the same.
  • The test file follows the pattern in tests/test_context_manager.py (pytest, no live server, mock the inner client for remote tests).
  • The repr for QdrantLocal should distinguish :memory: from a path. The persistent flag is already on the object, so a single conditional is enough.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions