Debugger-Safe Object Representation#
Problem#
When a Python debugger (debugpy / VS Code) hits a breakpoint, it suspends all Python threads and calls repr() on every local variable in the current frame. If __repr__ triggers async work through the LanceDBBackgroundEventLoop, the representation call blocks waiting for a thread that the debugger has already suspended β causing the debug session to freeze indefinitely.
The exact failure chain:
- Debugger stops at a breakpoint and suspends all Python threads.
- Debugger renders local variables by calling
repr(). - The old
LanceDBConnection.__repr__read theread_consistency_intervalproperty. - That property called
LOOP.run(...).result(). LanceDBBackgroundEventLoop's thread is suspended, soLOOP.run()deadlocks.
The symptom manifests immediately after lancedb.connect() because that is the first point at which a connection object exists as a local variable. The same problem existed in LanceTable.__repr__, which also accessed read_consistency_interval through the connection.
An earlier instance of this class of bug: LanceTable.__repr__ previously included version={self.version}, which triggers a round-trip to get the current table version β another property backed by LOOP.run().
Rule: __repr__ Must Be Side-Effect-Free#
The invariant is: __repr__ must never call LOOP.run() or perform any I/O. Debuggers, REPLs, and logging infrastructure all assume repr() is instant and non-blocking.
Current Implementations#
LanceDBConnection.__repr__ β reads only self._conn.uri, a plain string attribute set during construction:
LanceDBConnection(uri='/path/to/db')
LanceTable.__repr__ β reads only self.name and self._conn, both stored as instance attributes:
LanceTable(name='my_table', _conn=LanceDBConnection(uri='...'))
RemoteDBConnection.__repr__ β reads only self.db_name, stored at construction time:
RemoteConnect(name=my-database)
None of these access read_consistency_interval, version, or any other async-backed property.
Key Source Files#
| File | Class | __repr__ line |
|---|---|---|
python/python/lancedb/db.py | LanceDBConnection | L777β778 |
python/python/lancedb/table.py | LanceTable | L2492β2493 |
python/python/lancedb/remote/db.py | RemoteDBConnection | L182β183 |
The regression test that enforces the no-LOOP-in-repr contract lives in test_sync_repr_does_not_use_background_loop: it monkeypatches LOOP.run to raise, then asserts that both repr(db) and repr(table) still succeed.
Fix History#
| PR | What changed |
|---|---|
| #3411 | Removed version={self.version} from LanceTable.__repr__ (version fetch triggers a LOOP round-trip) |
| #3620 | Removed read_consistency_interval from both LanceDBConnection.__repr__ and LanceTable.__repr__; added regression test |
Guidelines When Adding Fields to __repr__#
Only include fields that:
- Are stored as plain attributes at construction time (e.g.,
self.uri,self.name). - Do not go through
LOOP.run(), anyawait, network I/O, or file I/O.
If you want to expose a computed/async value in a string representation, add a separate method (e.g., describe()) that callers invoke explicitly β never embed it in __repr__.
Setting PYDEVD_UNBLOCK_THREADS_TIMEOUT=0 in the debugger environment is a workaround that prevents indefinite blocking, but it does not fix the underlying issue.