About four months into a project, you start looking for the query that fetches a task by its ID. You find it in four different places: the route handler, a nightly job, a script for a one-time migration, and a test fixture. Three of these versions have changed over time. One still uses a column you renamed back in June.

That’s the mess a Data Access Object exists to prevent.
The idea is simple: put all database calls for a specific thing behind one object, give it a clear set of methods, and let the rest of your code use those methods instead of talking directly to the database. Your business logic decides what data it needs. The DAO figures out how to get it. As one good explanation puts it, the pattern separates what data you want from how it is stored.
Why every tutorial you find is written in Java
DAO started in Sun’s Core J2EE Patterns, which are enterprise Java best-practice guidelines. It developed alongside JDBC and relational databases, so when you search for it, you often find lots of StudentDaoImpl examples with getters and setters. The pattern itself is not tied to Java. That just happens to be where it originated.
At its core, it’s similar to the Adapter pattern. A concrete DAO takes a raw, awkward, low-level API like a SQL driver, an HTTP SDK, or a key-value client, and turns it into something your application actually wants to use.

What it looks like in Python
Three pieces. A data carrier, an interface, and at least one implementation.
from abc import ABC, abstractmethod
from dataclasses import dataclass
import sqlite3
@dataclass
class TaskDTO:
id: int
title: str
completed: bool
class TaskDAO(ABC):
@abstractmethod
def find_by_id(self, task_id: int) -> TaskDTO: ...
@abstractmethod
def save(self, task: TaskDTO) -> None: ...
class TaskDAOSqlite(TaskDAO):
def __init__(self, db_path: str):
self.conn = sqlite3.connect(db_path)
self.conn.execute(
"CREATE TABLE IF NOT EXISTS tasks (id INTEGER, title TEXT, completed INTEGER)"
)
def find_by_id(self, task_id: int) -> TaskDTO:
row = self.conn.execute(
"SELECT id, title, completed FROM tasks WHERE id = ?", (task_id,)
).fetchone()
return TaskDTO(row[0], row[1], bool(row[2])) if row else None
def save(self, task: TaskDTO) -> None:
self.conn.execute(
"INSERT OR REPLACE INTO tasks (id, title, completed) VALUES (?, ?, ?)",
(task.id, task.title, int(task.completed)),
)
self.conn.commit()TaskDTO is a Data Transfer Object. It’s just a simple container for fields, with no behavior beyond holding and passing along its own data. Next is TaskDAO, which acts as a contract. Finally, TaskDAOSqlite fulfills that contract in its own way.
Here’s the payoff. Write a TaskDAOPostgres tomorrow and every caller keeps working, because none of them ever knew SQLite was involved. Drop in a fake implementation during tests and your business logic runs without a database at all.

None of that setup is required, by the way. You can skip the abstract base class, wrap a Redis client in a class with two methods, and you still have a DAO. The pattern is about where you place the boundary, not about how formally you define it.
The part where Python people get suspicious
If you’ve used SQLAlchemy, something here probably feels redundant. Fair. Reasonable.
A common answer to this on Stack Overflow is that SQLAlchemy uses the Repository pattern, not the DAO pattern, so you don’t need to write DAO classes when you use it. This leads to the obvious question: what is the difference between the two?
Here’s the basic idea. A DAO is close to the storage engine, usually one per table, and works with records. A Repository sits at a higher level and uses the language of your domain, returning domain objects through a collection-like interface. According to Fowler, it acts as a bridge between the domain and data mapping layers. The clearest distinction I’ve seen is that a repository handles domain concepts, while a DAO handles technology-specific implementation details.
DZone explains the decision simply: DAO is a good choice when you want efficient data access without much domain complexity. A repository is better for applications where business logic is central and data operations need to match business meaning.
There’s a third relative in the family. Active Record bundles data and persistence behavior into one object, a model that knows how to save and load itself, the way Django’s ORM works. DAO deliberately pulls those apart.

So is the extra layer worth it?
Sometimes no. On a small project with one database and a dozen queries, a DAO is an interface, an implementation, and a stack of indirection buying you nothing. The boilerplate is real.
Abstractions leak, too, which quietly defeats the whole exercise. Once callers start reaching around the DAO for driver-specific behavior, you’ve paid for a boundary you aren’t actually enforcing.
But if you’re already using an ORM in Python, the real question isn’t whether you should adopt DAO. You’re already halfway there. The real question is whether adding a clear, hand-written layer on top is worth it. It is, if you want a single place for every query, an easy way to swap in tests, or the freedom to change storage engines without digging through your business logic.

Four copies of the same query, one of them wrong. Everything above is just bookkeeping about where the fix should live.


