-
-
Notifications
You must be signed in to change notification settings - Fork 444
Description
Describe the bug
QuerySet.in_bulk’s type hints do not match runtime behavior when field_name refers to a field type that is not str or int. For example UUIDField, BinaryField, DecimalField, etc.
The method is currently annotated roughly as:
id_list: Iterable[str | int]- return type:
dict[str, MODEL]
However, the implementation constructs the result as:
{getattr(obj, field_name): obj for obj in objs}This means the dictionary keys are typed values, not necessarily strings.
As a result:
- The input typing rejects valid runtime inputs such as
uuid.UUID,bytes,Decimal, etc. - The return typing is incorrect for non-
strfields. - In some cases the typing is actively misleading.
id_listelements are converted via the field’sto_db_value. For something likeBinaryField, passingstr(allowed by typing) can raiseTypeError: string argument without an encoding, while passingbytesworks but is rejected according to the types.
Field types likely affected:
Any field where the Python value type is not str, or where str is not reliably coercible, could encounter this issue.
UUIDField->uuid.UUIDBinaryField->bytesDecimalField->decimal.DecimalDatetimeField->datetime.datetimeDateField->datetime.dateTimeField->datetime.timeTimeDeltaField->datetime.timedeltaFloatField->float
The core issue seems to be generic, since keys are getattr(obj, field_name).
To Reproduce
- Define a model whose primary key has a non-
str|intPython type, for exampleUUIDField. - Create a single row and observe the Python type of the primary key value.
- Call
in_bulkusing the native Python value of the field and inspect the returned mapping’s key type.
Conceptual example:
item = await Item.create(...)
bulk = await Item.all().in_bulk([item.id], "id")
print(type(next(iter(bulk.keys()))))Observe that:
item.idis notstr(e.g. uuid.UUID)in_bulk([item.id], ...)works at runtime- the returned mapping keys have the same non-
strtype (e.g.uuid.UUID)
This contradicts the current type annotation, which specifies a return type of dict[str, MODEL] and restricts inputs to Iterable[str | int].
A self-contained reproducer using `uv` can be found in this spoiler tag
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# dependencies = ["tortoise-orm"]
# ///
import asyncio
import uuid
from tortoise import Tortoise, fields
from tortoise.models import Model
class Item(Model):
id = fields.UUIDField(primary_key=True, default=uuid.uuid4)
name = fields.CharField(max_length=100)
class Meta:
table = "items"
async def main() -> None:
await Tortoise.init(db_url="sqlite://:memory:", modules={"models": [__name__]})
await Tortoise.generate_schemas()
item = await Item.create(name="Test")
print("Item.id type :", type(item.id))
print("\nCall #1: pass str(UUID), allowed by typing")
bulk_str = await Item.all().in_bulk([str(item.id)], field_name="id")
key_str = next(iter(bulk_str.keys()))
print("Returned key type:", type(key_str))
print("Note above does not match expected str return")
print("\nCall #2: pass UUID directly, rejected by typing but still works")
bulk_uuid = await Item.all().in_bulk([item.id], field_name="id")
key_uuid = next(iter(bulk_uuid.keys()))
print("Returned key type:", type(key_uuid))
print("Note above also does not match expected str return")
await Tortoise.close_connections()
if __name__ == "__main__":
asyncio.run(main())Expected behavior
Type hints should reflect actual behavior:
id_listshould accept the native Python value type for the field, not onlystr | int.- The returned mapping key type should match the field’s Python value type, not always
str.
Additional context
This appears to be a typing/annotation issue rather than a runtime bug. The current runtime behavior of in_bulk seems to be consistent with expected value conversions (UUIDField -> uuid.UUID, BinaryField -> bytes, DecimalField -> Decimal, etc.).