Summary
Every public string argument refuses a str subclass such as numpy.str_, a (str, Enum) member or a StrEnum member. Two different errors come out, depending on where the value is checked:
-
Parameters typed str in Cython refuse at the call, with a clear message:
TypeError: Argument 'table_name' has incorrect type (expected str, got numpy.str_)
This applies to table_name (Sender.row, Sender.dataframe, QuestDB.dataframe, Sender.transaction, SenderTransaction), conf_str (Sender.from_conf, QuestDB.from_conf), host (Sender(...)), and sql (QuestDB.query, QuestDB.execute).
-
Values checked with isinstance(x, str) and then handed to a str-typed variable pass the check and fail on the next line. The error doesn't name the argument:
TypeError: Expected str, got numpy.str_
Over ILP it's wrapped as QuestDBError: Expected str, got numpy.str_. This applies to dataframe(at=...) and dataframe(symbols=[...]) (through _dataframe_get_loc(df, str col_name, ...)), and to the symbol and column names and values in Sender.row.
Both behaviors date from before 5.0. They aren't regressions.
Why it matters
numpy.str_ turns up whenever a name comes out of an array, e.g. at=np.array(['ts'])[0] or a table name taken from np.unique(df['tbl']). (str, Enum) is a common way to keep table and column names in one place. Both are str by every isinstance test, so the refusal is surprising. In case 2 the message doesn't even say which argument is at fault.
Reproduction
import numpy as np, pandas as pd, questdb
df = pd.DataFrame({'v': [1], 'ts': pd.to_datetime(['2025-01-01'])})
with questdb.connect('ws::addr=localhost:9000;') as db:
db.dataframe(df, table_name='t', at=np.str_('ts'))
# TypeError: Expected str, got numpy.str_
db.dataframe(df, table_name=np.str_('t'), at='ts')
# TypeError: Argument 'table_name' has incorrect type (expected str, got numpy.str_)
Suggested direction
Normalize every public string argument once, at the API boundary, then keep the internal str typing as it is:
- convert with
x if type(x) is str else str.__str__(x), not with str(x);
str() calls the subclass's own __str__, and for a (str, Enum) member that returns 'Kind.NAME' rather than the value.
Converting at the boundary also removes the inconsistency in case 2. Within one dataframe() call, at and table_name would then follow the same rule.
The df.attrs['questdb'] claim kind had the same problem. It was fixed in #140 (Moderate 5 of the 2026-09-30 review) by normalizing in _roundtrip_kind. That one couldn't wait, because a claim travels with the data and must never fail a write.
Summary
Every public string argument refuses a
strsubclass such asnumpy.str_, a(str, Enum)member or aStrEnummember. Two different errors come out, depending on where the value is checked:Parameters typed
strin Cython refuse at the call, with a clear message:This applies to
table_name(Sender.row,Sender.dataframe,QuestDB.dataframe,Sender.transaction,SenderTransaction),conf_str(Sender.from_conf,QuestDB.from_conf),host(Sender(...)), andsql(QuestDB.query,QuestDB.execute).Values checked with
isinstance(x, str)and then handed to astr-typed variable pass the check and fail on the next line. The error doesn't name the argument:Over ILP it's wrapped as
QuestDBError: Expected str, got numpy.str_. This applies todataframe(at=...)anddataframe(symbols=[...])(through_dataframe_get_loc(df, str col_name, ...)), and to the symbol and column names and values inSender.row.Both behaviors date from before 5.0. They aren't regressions.
Why it matters
numpy.str_turns up whenever a name comes out of an array, e.g.at=np.array(['ts'])[0]or a table name taken fromnp.unique(df['tbl']).(str, Enum)is a common way to keep table and column names in one place. Both arestrby everyisinstancetest, so the refusal is surprising. In case 2 the message doesn't even say which argument is at fault.Reproduction
Suggested direction
Normalize every public string argument once, at the API boundary, then keep the internal
strtyping as it is:x if type(x) is str else str.__str__(x), not withstr(x);str()calls the subclass's own__str__, and for a(str, Enum)member that returns'Kind.NAME'rather than the value.Converting at the boundary also removes the inconsistency in case 2. Within one
dataframe()call,atandtable_namewould then follow the same rule.The
df.attrs['questdb']claimkindhad the same problem. It was fixed in #140 (Moderate 5 of the 2026-09-30 review) by normalizing in_roundtrip_kind. That one couldn't wait, because a claim travels with the data and must never fail a write.