How Database Sessions Work
Understand the session lifecycle and how it replaces the old dict and save pattern
What changed between lessons
Before you start rewriting endpoints, notice what happened to main.py at the start of this lesson. The JSON storage code is gone:
save_expenses()andload_expenses()— removedexpenses = {}andcounter = 0— removedDATA_FILEand thedata_filesetting — removed- Unused imports (
json,os,pathlib) — removed
The endpoints still exist, but their bodies are all pass — placeholders waiting for database code. You will fill them in one by one across this lesson.
The session pattern
Every endpoint that reads from or writes to the database receives a session: SessionDep parameter. FastAPI calls get_session(), opens a session, and injects it before your endpoint runs.
Inside the endpoint, you work through the session:
| Operation | Call |
|---|---|
| Save a new record | session.add(obj) then session.commit() |
| Read by primary key | session.get(Model, id) |
| Run a query | session.exec(statement).all() |
| Delete a record | session.delete(obj) then session.commit() |
Why session.refresh()
After session.commit(), the object in memory is stale — the database has updated it (for example, assigning an id), but the Python object does not know yet. Calling session.refresh(expense) pulls the current state from the database back into the object.
You need this after creating or updating a record, any time you return the object in the response.
Comparing old and new
Here is create_expense before and after, so you can see the shape of the change:
# Before — dict + counter
def create_expense(expense: Expense):
global counter
counter += 1
expenses[counter] = {"id": counter, **expense.model_dump()}
save_expenses()
return expenses[counter]
# After — session
def create_expense(expense: Expense, session: SessionDep):
session.add(expense)
session.commit()
session.refresh(expense)
return expenseFour lines replace six. No global state, no file write, no manual ID assignment.