If you plan to develop locally against the Firestore emulator, install the optional emulator extra as well:
The library uses the official
google-cloud-firestore async client, which follows the standard Google Application Default Credentials (ADC) chain.You can also pass
emulator_host= directly to FirestoreDB at runtime instead of setting the environment variable—the library will configure the environment variable for you automatically.If you are running inside Google Cloud (Cloud Run, GKE, Cloud Functions, etc.) no extra configuration is needed—the metadata server provides credentials automatically.
Subclass
BaseFirestoreModel and declare the Firestore collection name in an inner Settings class. Add your document fields as ordinary Pydantic field annotations:from firestore_pydantic_odm import BaseFirestoreModel
class User(BaseFirestoreModel):
class Settings:
name = "users" # Firestore collection name
name: str
email: str
age: int = 0
Every model automatically gets an
id: Optional[str] field that stores the Firestore document ID. You do not need to declare it yourself.The
Settings.name attribute is required. If you omit it, the library falls back to the class name, which is usually not what you want for production collections.Create a
FirestoreDB instance and call init_firestore_odm() with it and a list of every model class your application uses. This step injects the database client into each model and registers the model graph for cascade-delete resolution:from firestore_pydantic_odm import FirestoreDB, init_firestore_odm
# Connect to the real Firestore backend
db = FirestoreDB(project_id="my-gcp-project")
# Or connect to the local emulator
db = FirestoreDB(project_id="my-gcp-project", emulator_host="localhost:8080")
# Register all models — pass every model class you intend to use
init_firestore_odm(db, [User])
Always call
init_firestore_odm() before performing any database operation. Attempting to call save(), find(), or any other async method before initialization raises a RuntimeError.All document operations are
async and must be awaited. Use save() to create, update() to persist changes, delete() to remove, and get() to fetch a document by its ID:import asyncio
from firestore_pydantic_odm import FirestoreDB, init_firestore_odm, BaseFirestoreModel
class User(BaseFirestoreModel):
class Settings:
name = "users"
name: str
email: str
async def crud_example():
db = FirestoreDB(project_id="my-gcp-project", emulator_host="localhost:8080")
init_firestore_odm(db, [User])
# CREATE — Firestore auto-generates the document ID
user = User(name="Alice", email="[email protected]")
await user.save()
print(f"Created user with id: {user.id}")
# UPDATE — mutate fields and call update()
user.email = "[email protected]"
await user.update()
# GET — retrieve a document by its Firestore document ID
fetched = await User.get(user.id)
print(f"Fetched: {fetched.name} — {fetched.email}")
# DELETE — permanently removes the document
await user.delete()
asyncio.run(crud_example())
find() is an async generator that streams matching documents. find_one() returns the first match or None. Both accept filters, order_by, limit, offset, and an optional projection:import asyncio
from pydantic import BaseModel
from firestore_pydantic_odm import (
FirestoreDB, init_firestore_odm, BaseFirestoreModel,
OrderByDirection,
)
class User(BaseFirestoreModel):
class Settings:
name = "users"
name: str
email: str
age: int = 0
# Projection — only fetch the fields you need
class UserProjection(BaseModel):
name: str
async def query_example():
db = FirestoreDB(project_id="my-gcp-project", emulator_host="localhost:8080")
init_firestore_odm(db, [User])
# Stream all users ordered by name ascending
async for u in User.find(order_by=[(User.name, OrderByDirection.ASCENDING)]):
print(u)
# Filter — find users aged 18 or older
async for u in User.find(filters=[User.age >= 18]):
print(u.name)
# Projection — Firestore only transfers the `name` field
async for u in User.find(
filters=[User.age >= 18],
projection=UserProjection,
order_by=[(User.name, OrderByDirection.ASCENDING)],
):
print(u.name) # u is a UserProjection instance
# find_one — return the first matching document
alice = await User.find_one(filters=[User.email == "[email protected]"])
if alice:
print(f"Found: {alice.name}")
asyncio.run(query_example())
Complete Working Example
The snippet below combines every step above into a singleasyncio.run() call you can copy, adjust the project_id and emulator_host, and run immediately:
Next Steps
Installation
Detailed install options, dependency versions, and credential setup for all environments.
Models
Learn how to define fields, aliases, nested objects, and model configuration.
Querying
Filters, ordering, pagination, projections, and collection-group queries.
Subcollections
Declare parent–child relationships and query deeply nested documents.
Batch Operations
Atomic multi-document writes and transactions.
Testing
Emulator setup and unit-test mocking strategies.
