Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Python›SQLAlchemy Basics

Popular Libraries Overview

SQLAlchemy Basics

SQLAlchemy is the definitive toolkit for database interactions in Python, offering both a high-level Object Relational Mapper and a low-level expression language. It abstracts complex storage systems into manageable Python objects, ensuring that your application logic remains decoupled from specific database implementations. You should reach for it whenever you need robust, scalable data persistence that prioritizes maintainability and type safety over raw string-based query construction.

Engine and Connection Setup

The Engine is the central interface for all database interactions in SQLAlchemy. It acts as a gateway that manages a pool of connections to the underlying database, optimizing performance by reusing connections rather than creating new ones for every single request. By providing a URL string, you instruct the engine on how to locate and authenticate with the database. When you call create_engine, SQLAlchemy does not immediately establish a connection; instead, it waits until a task requires one, at which point it fetches a connection from the internal pool. This lazy initialization is a fundamental pattern designed to minimize resource consumption during application startup. Understanding the engine is crucial because it governs the lifecycle of your interactions. You must always ensure that your database credentials and connection parameters are managed securely, typically through environment variables, to maintain high standards of security and operational portability in production environments.

from sqlalchemy import create_engine, text

# Configure the engine to connect to a local database
# The dialect is defined in the connection string
engine = create_engine('sqlite:///:memory:', echo=True)

# Open a connection to execute a simple command
with engine.connect() as connection:
    # text() wraps raw strings for safe execution
    result = connection.execute(text('SELECT 1'))
    print(result.fetchone())

Declarative Mapping

The Declarative system is the heart of the Object Relational Mapper, providing a way to map Python classes directly to database tables. By inheriting from a base class created by declarative_base, you define the schema of your data using Python class attributes. This approach is powerful because it treats database records as instances of your classes. When you define columns, you are essentially telling SQLAlchemy how to serialize and deserialize your Python objects into rows. This mapping is not just about structure; it defines relationships, constraints, and data types that ensure integrity at the application level. By keeping your data model within Python code, you gain the benefits of IDE autocompletion, type hinting, and easier refactoring. Changes to the schema are reflected in your code rather than hidden in migration scripts alone. The mapping mechanism uses a registry to keep track of these relationships, ensuring that objects remain consistent throughout the entire application runtime and across sessions.

from sqlalchemy.orm import declarative_base, Mapped, mapped_column

Base = declarative_base()

# Defining a user schema using mapped_column
class User(Base):
    __tablename__ = 'user_account'
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column()

print(User.__table__)

Sessions and Persistence

The Session object acts as a workspace for your data, serving as an identity map for all objects loaded from or saved to the database. When you add an object to a session, it becomes tracked, and any subsequent changes made to that object are automatically detected by the session. This is the unit of work pattern: instead of issuing an update query every time you modify an attribute, you perform all your logic within the session and call commit() to persist everything in a single transaction. This methodology significantly reduces network overhead and ensures that your operations are atomic. If an error occurs during processing, you can call rollback() to return the objects to their previous state, maintaining data consistency. The session is also responsible for managing the loading of related objects, ensuring that you do not accidentally lose the state of your application when database transactions are finalized. It is the primary tool for managing business logic transitions safely.

from sqlalchemy.orm import Session

# Create a session to manage database operations
with Session(engine) as session:
    new_user = User(name='Alice')
    session.add(new_user)
    # Commit persists changes to the database
    session.commit()
    print(f'User created with id: {new_user.id}')

Querying with Select

Querying is the process of retrieving data from the database, and SQLAlchemy provides a sophisticated interface through the select() function. Unlike raw string concatenations, which are prone to injection vulnerabilities, select() builds a tree of Python objects that represents the query structure. This tree is then compiled into a dialect-specific query string. This abstraction layer is vital because it allows the developer to write complex filtering, ordering, and joining logic using Python syntax, which is much easier to maintain. When you execute a select statement, SQLAlchemy returns a result object that provides iteration and fetching capabilities. This iterator approach is memory efficient; it allows you to process large result sets without loading the entire collection into application memory at once. By chaining methods like where(), order_by(), and limit(), you can construct highly dynamic queries based on runtime conditions, all while maintaining complete type safety and structural integrity throughout the data retrieval process.

from sqlalchemy import select

with Session(engine) as session:
    # Construct a query to find a specific user
    stmt = select(User).where(User.name == 'Alice')
    # Execute returns a result object
    result = session.scalars(stmt).first()
    print(f'Found user: {result.name}')

Relationships and Joins

Managing relationships between tables is a core strength of SQLAlchemy, specifically handled by the relationship() construct. This feature allows you to define associations between mapped classes, such as a one-to-many relationship where one User might have many Addresses. By defining these associations, you enable lazy loading, which fetches related data only when you access the attribute for the first time. This significantly improves application performance by avoiding unnecessary database queries. Behind the scenes, the relationship manager keeps track of foreign key constraints and join conditions, automating the generation of complex JOIN statements when you perform queries. Understanding this is essential because it allows you to navigate your data model as a graph of objects. You can traverse from a user to their collection of addresses just by accessing a property, without ever needing to write manual join logic. This encapsulation of relational complexity is what makes object-oriented data design both elegant and extremely productive in enterprise applications.

from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship

class Address(Base):
    __tablename__ = 'address'
    id: Mapped[int] = mapped_column(primary_key=True)
    user_id: Mapped[int] = mapped_column(ForeignKey('user_account.id'))
    user: Mapped['User'] = relationship()

# Now User can access addresses via a relationship property

Key points

  • The engine serves as the central factory for database connections and transaction management.
  • Declarative mapping bridges the gap between Python classes and relational database tables.
  • The Session object implements the unit of work pattern to manage atomic operations.
  • Lazy initialization ensures that database connections are only created when truly necessary.
  • The select() function provides a secure and structured way to retrieve data without raw strings.
  • Relationships automate the complex logic required for joining related tables in the database.
  • Commit and rollback are essential methods for maintaining data consistency during sessions.
  • Using objects instead of raw queries improves code readability and reduces injection vulnerabilities.

Common mistakes

  • Mistake: Forgetting to call session.commit(). Why it's wrong: Changes made to objects are tracked in the session but not persisted to the database until committed. Fix: Always call session.commit() after adding or updating objects.
  • Mistake: Using raw SQL queries instead of the ORM methods. Why it's wrong: This defeats the purpose of an abstraction layer, loses security against injection, and makes code database-dependent. Fix: Use session.query() and model methods.
  • Mistake: Accessing lazy-loaded attributes outside of an active session. Why it's wrong: SQLAlchemy closes the session connection when the block ends, leading to DetachedInstanceError. Fix: Use joined loading or keep the session open during data access.
  • Mistake: Modifying primary key values of objects after they are tracked by the session. Why it's wrong: The session relies on the primary key to map objects; changing it breaks identity tracking. Fix: Never manually update primary key attributes.
  • Mistake: Opening a new session for every single database operation. Why it's wrong: This causes significant overhead due to constant connection re-establishment. Fix: Utilize a scoped session or a context manager for the request lifecycle.

Interview questions

What is SQLAlchemy and why do we use it in Python applications?

SQLAlchemy is the premier Object-Relational Mapper (ORM) for Python. We use it because it abstracts away the complexities of raw SQL, allowing developers to interact with databases using Python classes and objects. By mapping database tables to Python classes, SQLAlchemy ensures that our code remains readable and maintainable. It effectively bridges the gap between the object-oriented nature of Python and the relational nature of SQL databases, reducing boilerplate code significantly.

How do you connect to a database using SQLAlchemy in a Python script?

To connect to a database, you first use the 'create_engine' function provided by the sqlalchemy library. You pass a database URL, which is a string containing the dialect, driver, username, password, host, and database name. For example: 'engine = create_engine('sqlite:///example.db')'. This engine serves as the central interface for all database connectivity. It does not connect immediately; instead, it creates a pool of connections that are managed efficiently as you execute queries.

What is a Declarative Base in SQLAlchemy, and why is it essential?

In SQLAlchemy, the Declarative Base is a base class that keeps a catalog of classes and tables relative to that base. We typically define it by calling 'declarative_base()'. When we create models, we inherit from this base class, which allows SQLAlchemy to automatically map our Python class attributes to table columns. This is essential because it eliminates the need to manually define table schemas in SQL, keeping the database schema definition tightly coupled with our Python model definitions.

How does a SQLAlchemy Session work, and why must we commit changes?

A Session acts as a workspace for all the objects you have loaded or associated with the database. It maintains an 'identity map', which ensures that you only have one object instance for a specific database row. When you add objects to the session, they are in a pending state. We must call 'session.commit()' to persist these changes because it triggers a flush, which sends the SQL statements to the database and finalizes the transaction permanently.

Compare using SQLAlchemy's ORM layer versus the Core Expression Language.

The ORM layer is designed to manage high-level object persistence, where database rows are represented as Python objects, making it ideal for domain-driven applications. Conversely, the Core Expression Language is a lower-level SQL abstraction that focuses on programmatic SQL construction. Choose ORM for rapid development and object manipulation, but choose Core if you need high-performance bulk operations or very complex, non-standard SQL queries where the overhead of object instantiation is undesirable.

Explain how to handle relationships between models using SQLAlchemy.

Relationships are managed using the 'relationship()' function and Foreign Keys. You define a foreign key on the child model to link to the parent's primary key, and then use 'relationship()' on the parent to access the child objects directly as a Python list or attribute. This is powerful because it allows you to navigate data graphs using dot notation, such as 'user.posts', which SQLAlchemy fetches lazily or eagerly depending on your configuration settings.

All Python interview questions →

Check yourself

1. When defining a model, what is the primary purpose of the 'Column' type definition?

  • A.To execute a command that creates a table immediately
  • B.To define the data type and constraints for a database field
  • C.To validate data types at runtime in Python
  • D.To create a primary key automatically regardless of configuration
Show answer

B. To define the data type and constraints for a database field
Option 2 is correct because Column maps Python attributes to database schema definitions. Option 1 is wrong because table creation requires metadata.create_all(). Option 3 is wrong because Column does not enforce runtime type validation. Option 4 is wrong because primary keys must be explicitly marked.

2. What is the result of session.add(my_object) in SQLAlchemy?

  • A.The object is immediately written to the database disk
  • B.The object is marked as 'pending' and added to the current session's identity map
  • C.The object is validated against the database schema constraints
  • D.The object is automatically committed to the database
Show answer

B. The object is marked as 'pending' and added to the current session's identity map
Option 2 is correct because add() transitions the object into the session state. Option 1 is wrong because commit() is required for persistence. Option 3 is wrong because validation usually happens upon flush or commit. Option 4 is wrong because SQLAlchemy follows an explicit commit pattern.

3. Why would you choose to use 'joinedload' in a query?

  • A.To improve performance by reducing the number of queries for related objects
  • B.To force the database to merge two tables into a new physical table
  • C.To create a bidirectional relationship between two models
  • D.To delete related records automatically when a parent is deleted
Show answer

A. To improve performance by reducing the number of queries for related objects
Option 1 is correct as it solves the 'N+1' query problem. Option 2 is wrong because joinedload only affects the SELECT statement result. Option 3 is wrong because that is handled by the relationship() function. Option 4 is wrong because that is handled by the cascade setting.

4. What happens if you execute session.rollback()?

  • A.It removes all records from the database table
  • B.It reverts the session to its state before the current transaction started
  • C.It deletes the Python objects currently in memory
  • D.It performs a clean shutdown of the database engine
Show answer

B. It reverts the session to its state before the current transaction started
Option 2 is correct because rollback undoes uncommitted changes. Option 1 is wrong because it only affects the transaction, not existing table data. Option 3 is wrong because objects remain in memory but their session state resets. Option 4 is wrong because it does not disconnect the engine.

5. How does the 'relationship' function differ from a 'ForeignKey'?

  • A.ForeignKey creates the relationship, relationship is optional metadata
  • B.ForeignKey handles the database structure, relationship handles Python object navigation
  • C.They are identical and can be used interchangeably
  • D.Relationship is used for integers while ForeignKey is used for strings
Show answer

B. ForeignKey handles the database structure, relationship handles Python object navigation
Option 2 is correct because ForeignKeys are for database schema linking, while relationship() enables high-level ORM navigation. Option 1 is wrong because both are needed for a full ORM experience. Option 3 is wrong because they serve different layers. Option 4 is wrong because type definitions are not based on the function name.

Take the full Python quiz →

← PreviousMatplotlib — Basic PlottingNext →FastAPI Quick Overview

Python

78 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app