Yes. Your understanding is already quite good. The only missing piece is why each object exists and how they all connect together.
Think of it as building a house before people start living in it.
Step 1
Tell Python where database is
DATABASE_URL
│
▼
sqlite:///./users.db
Step 2
Build connection engine
create_engine()
│
▼
engine
Step 3
Build session factory
sessionmaker(bind=engine)
│
▼
SessionLocal
Step 4
Create ORM Base class
Base = declarative_base()
│
▼
class User(Base)
│
▼
Table Definition
Step 5
Tell SQLAlchemy to create tables
Base.metadata.create_all(bind=engine)
│
▼
users.db
│
▼
users Table
Step 6
Whenever request comes
db = SessionLocal()
│
▼
Read / Write Database
Notice that Steps 1–5 happen only once (during application startup), while Step 6 happens for every request.
DATABASE_URL = "sqlite:///./users.db"
This is simply the database's address.
Imagine you're telling a delivery person:
"My house is at 25 Green Street."
Similarly:
sqlite:///./users.db
means
My database is stored in the file users.db.
Nothing is created yet.
You're only specifying where the database is located.
engine = create_engine(DATABASE_URL)
The engine is not the database.
It is a connection manager.
Think of it like this:
Python
│
▼
Engine
│
▼
users.db
The engine knows:
where the database is
how to connect
how to send SQL
how to receive results
Without the engine,
Python cannot talk to SQLite.
Imagine your database is another country.
The engine is the airplane.
Without the airplane,
you cannot travel there.
SessionLocal = sessionmaker(bind=engine)
This is one of the biggest concepts beginners struggle with.
Notice carefully:
This does NOT create a database session.
It creates a factory that knows how to create sessions.
Think of a cookie factory.
Cookie Factory
│
Makes Cookies
Similarly,
Session Factory
│
Makes Sessions
So
SessionLocal
is NOT a session.
It is a machine that creates sessions.
Later,
db = SessionLocal()
creates an actual session.
Exactly like
CookieFactory()
↓
Cookie
Base = declarative_base()
This tells SQLAlchemy:
"Every class inheriting from Base is a database table."
Without Base,
class User:
is just a normal Python class.
With Base,
class User(Base):
SQLAlchemy immediately understands:
This represents a database table.
class User(Base):
Now you describe the table.
__tablename__ = "users"
means
Database table name:
users
Then
id = Column(...)
means
id column
Then
username = Column(...)
means
username column
Then
hashed_password = Column(...)
means
hashed_password column
At this point,
the table still doesn't exist.
You're only describing it.
Think of drawing a house blueprint.
The house hasn't been built yet.
Base.metadata.create_all(bind=engine)
This is where SQLAlchemy says
"Okay, I've seen all the blueprints."
Now it asks:
Does users table exist?
If NO,
it creates it.
If YES,
it skips it.
Equivalent SQL:
CREATE TABLE users(
id INTEGER PRIMARY KEY,
username TEXT,
hashed_password TEXT
);
Now your database becomes
users.db
│
└── users table
Notice something.
Until now,
no user exists.
Only the table exists.
users
-----------------------
(empty)
Suppose someone signs up.
FastAPI calls
get_db()
Inside
db = SessionLocal()
Remember?
SessionLocal is the factory.
Session Factory
↓
Creates Session
Now
db
is your active conversation with the database.
Imagine
Python
│
db
│
Engine
│
users.db
new_user = User(...)
creates a Python object.
Not a database row.
Just a Python object.
Memory
↓
User Object
Then
db.add(new_user)
tells SQLAlchemy
"Prepare to insert this."
Still not saved.
Then
db.commit()
actually writes it into
users.db
Finally,
db.close()
ends the conversation.
Exactly like hanging up a phone call.
Instead of writing
db = SessionLocal()
everywhere,
we create one reusable function.
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
Every request gets:
Open Session
↓
Use Database
↓
Close Session
Automatically.
Developer Starts FastAPI
│
▼
DATABASE_URL
│
▼
create_engine()
│
▼
engine
│
▼
sessionmaker()
│
▼
SessionLocal (Factory)
│
▼
declarative_base()
│
▼
class User(Base)
│
▼
create_all()
│
▼
users.db
│
▼
users table created
═══════════════════════════════════════
User sends Signup Request
│
▼
get_db()
│
▼
db = SessionLocal()
│
▼
Create User Object
│
▼
db.add()
│
▼
db.commit()
│
▼
Row Stored in users.db
│
▼
db.close()
There are three different layers, and keeping them separate removes most confusion:
Database configuration (created once): DATABASE_URL → engine → SessionLocal
Database schema (created once): Base → User model → Base.metadata.create_all(bind=engine)
Database operations (every request): db = SessionLocal() → query/add/update/delete → db.commit() (if needed) → db.close()
Once you see these as Configuration → Schema → Runtime Operations, the entire SQLAlchemy workflow becomes much easier to reason about.