Skip to content

Fix PrivateAttr defaults not initialized when loading from DB#1

Open
veeceey wants to merge 1 commit intomainfrom
fix/issue-149
Open

Fix PrivateAttr defaults not initialized when loading from DB#1
veeceey wants to merge 1 commit intomainfrom
fix/issue-149

Conversation

@veeceey
Copy link
Owner

@veeceey veeceey commented Feb 8, 2026

Summary

Fixes fastapi#149 - Pydantic.PrivateAttr default and default_factory are ignored by SQLModel.

When SQLAlchemy loads objects from the database, it bypasses __init__ and only calls __new__. The init_pydantic_private_attrs function was setting __pydantic_private__ to None, which caused accessing any PrivateAttr to raise:

TypeError: 'NoneType' object is not subscriptable

Changes

  • sqlmodel/_compat.py: Updated init_pydantic_private_attrs() to properly iterate over __private_attributes__ and populate __pydantic_private__ with default values, matching Pydantic's own init_private_attributes behavior. Also fixed the same issue in sqlmodel_table_construct() where __pydantic_private__ was set to None when __pydantic_post_init__ is falsy.

  • tests/test_private_attr.py: Added 6 comprehensive tests covering:

    • PrivateAttr(default=...) on direct construction
    • PrivateAttr(default_factory=...) on direct construction
    • PrivateAttr(default=...) after loading from DB
    • PrivateAttr(default_factory=...) after loading from DB
    • Property access pattern over PrivateAttr after DB load
    • PrivateAttr() without defaults is correctly excluded from the dict

Before this fix

class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    secret_name: str
    _name: str = PrivateAttr(default="default_hero")

# Loading from DB:
hero = session.exec(select(Hero)).first()
hero._name  # TypeError: 'NoneType' object is not subscriptable
hero.__pydantic_private__  # None

After this fix

hero = session.exec(select(Hero)).first()
hero._name  # "default_hero"
hero.__pydantic_private__  # {'_name': 'default_hero'}

Test plan

  • All 81 existing core tests pass (no regressions)
  • 6 new tests for PrivateAttr behavior all pass
  • Verified fix with PrivateAttr(default=...), PrivateAttr(default_factory=...), and PrivateAttr() (no default)
  • Verified the property access pattern from issue comments works correctly

When SQLAlchemy loads objects from the database, it bypasses __init__
and only calls __new__. The init_pydantic_private_attrs function was
setting __pydantic_private__ to None, which caused accessing any
PrivateAttr to raise "TypeError: 'NoneType' object is not subscriptable".

Now properly initializes __pydantic_private__ with default values from
__private_attributes__, matching Pydantic's own init_private_attributes
behavior. Also fixes the same issue in sqlmodel_table_construct.

Fixes fastapi#149

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@veeceey veeceey changed the title 🐛 Fix PrivateAttr defaults not initialized when loading from DB Fix PrivateAttr defaults not initialized when loading from DB Feb 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pydantic.PrivateAttr default and default_factory are ignored by SQLModel

1 participant