Uh oh!
There was an error while loading. Please reload this page.
🐛 Declare __tablename__ on the metaclass (improve pyright compatibility) - #1821
🐛 Declare __tablename__ on the metaclass (improve pyright compatibility)#1821estarfoo wants to merge 1 commit into
__tablename__ on the metaclass (improve pyright compatibility)#1821Conversation
YuriiMotov
commented
Mar 31, 2026
estarfoo
commented
Jun 8, 2026
@YuriiMotov, thank you very much for the review! Sorry for being slow to get back to this. I agree that the dynamic table name is a valid use case and deserves support along with the static case. However:
fromsqlalchemy.orm.decl_apiimport_declared_directive# …__tablename__: ClassVar[str|Callable[..., str] |_declared_directive[str]]
__tablename__: ClassVar[Any]But the second option loses typing for table names. Any preference, or do you maybe see another way? |
The `SQLModel` base declared `__tablename__` both as `ClassVar[str | Callable[..., str]]` and as a `@declared_attr` method. Type checkers (pyright) see the descriptor type from `@declared_attr`, so `__tablename__ = "my_table"` in a subclass is rejected as a type mismatch even though it works at runtime; and the `ClassVar` on the base both leaks into the constructor signature and conflicts with a descriptor override such as `@declared_attr.directive`. Move the declaration to `SQLModelMetaclass` as `__tablename__: str`, setting the default in `SQLModelMetaclass.__new__` (in the class dict, before class creation) unless the user supplied one. Because the attribute now lives on the metaclass: - explicit names (`__tablename__ = "my_table"`) narrow to `str` rather than the `str | Callable` union, so reads are usable without casts; - it is not collected as a model field, so it never appears in the constructor; - a dynamically computed name via `@declared_attr.directive` type-checks in pyright basic mode, leaving only a single reportIncompatibleVariableOverride in standard/strict mode, inherent to overriding a class attribute with a descriptor. Tests added for default name, explicit override, inheritance, non-table models, and the `@declared_attr.directive` form. Fixesfastapi#98. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
24d66f7 to
415c8fcCompare__tablename__ default from @declared_attr to metaclass__tablename__ on the metaclass (improve pyright compatibility)estarfoo
commented
Jun 10, 2026
@YuriiMotov, following up on dynamic table names: neither option from above turned out to be satisfying, so here's a rework shifting the The current version would still produce a pyright message. I don't see a way around this at the moment, and I think it's an intrinsic limitation coming from SQLAlchemy. I'm open to suggestions on this, though! |

Move
__tablename__from a@declared_attrmethod onSQLModelto a declaration onSQLModelMetaclass, with the default set inSQLModelMetaclass.__new__before class creation (unless the user supplied one).This resolves the type-level contradiction between the
ClassVar[str | Callable]declaration and the@declared_attrdescriptor, letting__tablename__ = "my_table"work without# type: ignorefor pyright.Caveat: dynamically computed names via
@declared_attr.directive(per @YuriiMotov's review below) type-check in pyright basic mode. In standard/strict mode, onereportIncompatibleVariableOverridediagnostic remains.Tests added for default name, explicit override, inheritance, non-table models, and the
@declared_attr.directiveform.Fixes#98.
This is split out from #1820, dropping those changes which would be resolved by #1345 or #1806.
(Description edited following source branch update from 24d66f7 to 415c8fc.)