
CRUD em Python com SQLite — passo a passo para iniciantes
Vamos construir um mini-app de tarefas (to-do) no terminal usando Python + SQLite. A ideia é você entender cada parte do código, do zero: conectar, criar tabela, inserir, listar, atualizar e excluir (CRUD). Então, no caminho, explicarei decisões de projeto, boas práticas e como ler erros comuns.
0) Pré-requisitos mínimos
Antes de tudo, você precisa ter o Python 3 instalado (o módulo padrão sqlite3 já vem junto). Então, para saber abrir um terminal e executar python --version (ou python3 --version em alguns sistemas).
1) O que é SQLite (sem mistério)
- É um banco de dados que vive em um único arquivo (ex.:
tarefas.sqlite). - Contudo, não precisa de servidor rodando.
- Além disso, ele é excelente para projetos pequenos, scripts, estudos e protótipos.
Para nosso projeto: tudo que cadastrarmos (tarefas) ficará dentro de um arquivo .sqlite. Por isso, se você apagar esse arquivo, apaga os dados.
Observação: SQLite é “serverless”, mas ainda assim é um banco relacional com SQL “de verdade”, constraints, transações e índices.
2) Comece pelo começo: conectando e criando a tabela
A princípio, crie uma pasta, por exemplo crud_sqlite, e depois, dentro dela crie passo1.py:
# passo1.py
import sqlite3
from pathlib import Path
DB_PATH = Path("tarefas.sqlite")
SQL_CRIAR_TABELA = """
CREATE TABLE IF NOT EXISTS tarefas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
descricao TEXT,
status TEXT NOT NULL DEFAULT 'aberta',
criado_em TEXT NOT NULL,
atualizado_em TEXT
);
"""
# 1) abre (ou cria) o arquivo do banco
con = sqlite3.connect(DB_PATH)
# 2) cria um "cursor" para enviar comandos SQL
cur = con.cursor()
# 3) cria a tabela se não existir
cur.execute(SQL_CRIAR_TABELA)
# 4) salva a alteração (commit) e fecha
con.commit()
con.close()
print("Banco inicializado: tarefas.sqlite e tabela criada (se não existia).")
Entenda cada linha:
sqlite3.connect(DB_PATH): abre o arquivo do banco (ou cria se não existir).cursor(): objeto que envia comandos SQL.CREATE TABLE IF NOT EXISTS: evita recriar a tabela sempre.commit(): confirma alterações (DDL/INSERT/UPDATE/DELETE).close(): fecha a conexão corretamente.
Rode:
python passo1.py
Se não deu erro, o arquivo tarefas.sqlite apareceu ao lado do script.
3) Inserindo dados (Create)
Vamos inserir uma tarefa. Então, crie passo2.py:
# passo2.py
import sqlite3
from pathlib import Path
from datetime import datetime
DB_PATH = Path("tarefas.sqlite")
con = sqlite3.connect(DB_PATH)
cur = con.cursor()
# Usamos placeholders (?) para evitar SQL Injection e converter tipos
sql = "INSERT INTO tarefas (titulo, descricao, status, criado_em) VALUES (?, ?, ?, ?)"
dados = (
"Estudar Python",
"Ler sobre CRUD e SQLite",
"aberta",
datetime.now().isoformat(timespec="seconds")
)
cur.execute(sql, dados)
con.commit()
print("Inserido! ID gerado:", cur.lastrowid)
con.close()
Por que desse jeito?
- Placeholders
?: nunca concatene valores do usuário na SQL; use parâmetros. cur.lastrowid: pega o ID gerado automaticamente.datetime.now().isoformat(...): padrão legível e consistente (ISO-8601).
Rode:
python passo2.py
4) Consultando dados (Read)
Agora, vamos listar o que está no banco. Para isso, crie passo3.py:
# passo3.py
import sqlite3
from pathlib import Path
DB_PATH = Path("tarefas.sqlite")
# Dica: row_factory permite ler por nome da coluna
con = sqlite3.connect(DB_PATH)
con.row_factory = sqlite3.Row
cur = con.cursor()
cur.execute("SELECT * FROM tarefas ORDER BY id")
linhas = cur.fetchall()
if not linhas:
print("Não há tarefas.")
else:
for linha in linhas:
# linha é um "Row", dá pra usar dict(linha)
t = dict(linha)
print(f"[{t['id']}] {t['titulo']} | {t['status']} (criado: {t['criado_em']})")
con.close()
Aprendizados:
con.row_factory = sqlite3.Row: permite acessar colunas por nome.fetchall(): busca todas as linhas.ORDER BY id: mantém a listagem previsível.
pronto, então rode:
python passo3.py
5) Atualizando (Update)
Precisamos de meio para atualizar nossos registros. Por isso, crie agora passo4.py para alterar título/descrição/status de uma tarefa pelo ID:
# passo4.py
import sqlite3
from pathlib import Path
from datetime import datetime
DB_PATH = Path("tarefas.sqlite")
def atualizar_tarefa(tarefa_id, titulo=None, descricao=None, status=None):
con = sqlite3.connect(DB_PATH)
cur = con.cursor()
campos = []
valores = []
if titulo is not None:
campos.append("titulo = ?")
valores.append(titulo.strip())
if descricao is not None:
campos.append("descricao = ?")
valores.append(descricao.strip())
if status is not None:
campos.append("status = ?")
valores.append(status)
if not campos:
print("Nada para atualizar.")
con.close()
return False
campos.append("atualizado_em = ?")
valores.append(datetime.now().isoformat(timespec="seconds"))
valores.append(tarefa_id)
sql = f"UPDATE tarefas SET {', '.join(campos)} WHERE id = ?"
cur.execute(sql, valores)
con.commit()
alteradas = cur.rowcount
con.close()
return alteradas > 0
if __name__ == "__main__":
ok = atualizar_tarefa(
tarefa_id=1,
status="concluida",
descricao="Terminei o estudo de CRUD!"
)
print("Atualizada." if ok else "Nada foi alterado (ID existe?).")
Estratégia usada:
- Montamos
camposapenas com o que o usuário quer mudar. - Usamos
WHERE id = ?para alterar um registro. rowcountdiz quantas linhas foram afetadas.
Para testar, rode:
python passo4.py
Mas, depois liste de novo com passo3.py.
6) Excluindo (Delete)
Agora, crie passo5.py:
# passo5.py
import sqlite3
from pathlib import Path
DB_PATH = Path("tarefas.sqlite")
def excluir_tarefa(tarefa_id):
con = sqlite3.connect(DB_PATH)
cur = con.cursor()
cur.execute("DELETE FROM tarefas WHERE id = ?", (tarefa_id,))
con.commit()
removidas = cur.rowcount
con.close()
return removidas > 0
if __name__ == "__main__":
ok = excluir_tarefa(1)
print("Excluída." if ok else "Não encontrada.")
Agora, rode:
python passo5.py
Então, para finalizar essa etapa, confira o resultado desse teste com passo3.py.
7) Juntando tudo em um mini-app (um arquivo só)
Visto que, você já entendeu cada operação separada, então, vamos então consolidar tudo em um só arquivo com funções CRUD e um menu de terminal.
Primeiro, crie o arquivo app.py:
import sqlite3
from pathlib import Path
from datetime import datetime
DB_PATH = Path("tarefas.sqlite")
SQL_CRIAR_TABELA = """
CREATE TABLE IF NOT EXISTS tarefas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
descricao TEXT,
status TEXT NOT NULL DEFAULT 'aberta' CHECK (status IN ('aberta','fazendo','concluida')),
criado_em TEXT NOT NULL,
atualizado_em TEXT
);
"""
def obter_conexao():
con = sqlite3.connect(DB_PATH)
con.row_factory = sqlite3.Row
return con
def inicializar_banco():
with obter_conexao() as con:
con.execute(SQL_CRIAR_TABELA)
con.commit()
# ---------------- CRUD ----------------
def criar_tarefa(titulo, descricao="", status="aberta"):
titulo = (titulo or "").strip()
if not titulo:
raise ValueError("Título é obrigatório.")
if status not in ("aberta", "fazendo", "concluida"):
raise ValueError("Status inválido.")
agora = datetime.now().isoformat(timespec="seconds")
with obter_conexao() as con:
cur = con.execute(
"INSERT INTO tarefas (titulo, descricao, status, criado_em) VALUES (?, ?, ?, ?)",
(titulo, (descricao or "").strip(), status, agora),
)
con.commit()
return cur.lastrowid
def listar_tarefas(status=None):
with obter_conexao() as con:
if status:
cur = con.execute(
"SELECT * FROM tarefas WHERE status = ? ORDER BY id", (status,))
else:
cur = con.execute("SELECT * FROM tarefas ORDER BY id")
return [dict(l) for l in cur.fetchall()]
def obter_tarefa_por_id(tarefa_id: int):
with obter_conexao() as con:
cur = con.execute("SELECT * FROM tarefas WHERE id = ?", (tarefa_id,))
row = cur.fetchone()
return dict(row) if row else None
def atualizar_tarefa(tarefa_id: int, *, titulo=None, descricao=None, status=None):
campos, valores = [], []
if titulo is not None:
t = titulo.strip()
if not t:
raise ValueError("Título não pode ser vazio.")
campos.append("titulo = ?")
valores.append(t)
if descricao is not None:
campos.append("descricao = ?")
valores.append(descricao.strip())
if status is not None:
if status not in ("aberta", "fazendo", "concluida"):
raise ValueError("Status inválido.")
campos.append("status = ?")
valores.append(status)
if not campos:
return False
campos.append("atualizado_em = ?")
valores.append(datetime.now().isoformat(timespec="seconds"))
valores.append(tarefa_id)
sql = f"UPDATE tarefas SET {', '.join(campos)} WHERE id = ?"
with obter_conexao() as con:
cur = con.execute(sql, valores)
con.commit()
return cur.rowcount > 0
def excluir_tarefa(tarefa_id: int):
with obter_conexao() as con:
cur = con.execute("DELETE FROM tarefas WHERE id = ?", (tarefa_id,))
con.commit()
return cur.rowcount > 0
# ---------------- CLI ----------------
def mostrar_tarefa(t):
print("— Detalhes da tarefa —")
for k in ("id", "titulo", "descricao", "status", "criado_em", "atualizado_em"):
print(f"{k}: {t.get(k)}")
def menu():
print("\n=== Tarefas (SQLite) ===")
print("1. Criar tarefa")
print("2. Listar tarefas (opcional: filtrar por status)")
print("3. Ver tarefa por ID")
print("4. Atualizar tarefa")
print("5. Excluir tarefa")
print("0. Sair")
return input("Escolha: ").strip()
def main():
inicializar_banco()
while True:
opc = menu()
try:
if opc == "1":
titulo = input("Título: ")
descricao = input("Descrição (opcional): ")
status = input("Status [aberta/fazendo/concluida] (Enter=aberta): ").strip() or "aberta"
novo_id = criar_tarefa(titulo, descricao, status)
print(f"Tarefa criada com ID {novo_id}.")
elif opc == "2":
filtro = input("Filtrar por status (Enter = todos): ").strip() or None
tarefas = listar_tarefas(filtro)
if not tarefas:
print("Sem tarefas.")
else:
for t in tarefas:
print(f"[{t['id']}] {t['titulo']} | {t['status']} (criado: {t['criado_em']})")
elif opc == "3":
tid = int(input("ID: "))
t = obter_tarefa_por_id(tid)
if t:
mostrar_tarefa(t)
else:
print("Não encontrada.")
elif opc == "4":
tid = int(input("ID: "))
novo_titulo = input("Novo título (Enter = manter): ").strip()
nova_desc = input("Nova descrição (Enter = manter): ").strip()
novo_status = input("Novo status [aberta/fazendo/concluida] (Enter = manter): ").strip()
ok = atualizar_tarefa(
tid,
titulo=novo_titulo or None,
descricao=nova_desc or None,
status=novo_status or None,
)
print("Atualizada." if ok else "Nada alterado (ID existe?).")
elif opc == "5":
tid = int(input("ID: "))
ok = excluir_tarefa(tid)
print("Excluída." if ok else "Não encontrada.")
elif opc == "0":
print("Até mais!")
break
else:
print("Opção inválida.")
except ValueError as e:
print("Entrada inválida:", e)
except sqlite3.IntegrityError as e:
print("Regra do banco violada:", e)
if __name__ == "__main__":
main()
Finalmente, rode:
python app.py
Mas, por que esse app é mais “robusto”?
row_factory = sqlite3.Row: facilita imprimir por nome.- Constraint
CHECKnostatus: o banco impede valores fora do conjunto. - Além disso, temos Validações no Python (
ValueError) antes de gravar. with obter_conexao(): abre/fecha conexão automaticamente;commit()garante persistência.- Ainda, temos funções CRUD separadas da camada de CLI (menu), facilitando testes e reaproveitamento depois (ex.: API).
8) O que é transação? Por que commit()?
Uma transação é um conjunto de operações que deve ser executado como uma unidade.
- Por isso, se tudo deu certo →
commit()confirma. - Mas, se algo falhou → você pode chamar
rollback()(voltar atrás).
Portanto, no nosso fluxo, cada operação crítica é feita dentro de um bloco with e concluída com commit().
Curiosidade: por padrão, sqlite3 trabalha fora do modo autocommit. Logo, não chamar commit() (ou sair do with) implica perder alterações.
9) Tratando erros comuns (e entendendo a causa)
sqlite3.OperationalError: no such table: tarefas
Você esqueceu de rodarinicializar_banco()(ou opasso1.py).
Solução: crie a tabela antes de inserir/consultar.ValueError: Título é obrigatório.
Otituloveio vazio.
Solução: informe um texto não vazio.sqlite3.IntegrityError: CHECK constraint failed
Ostatusnão eraaberta,fazendoouconcluida.
Solução: valide o valor antes de inserir/atualizar.- Arquivo
.sqlite“sumiu”
Você rodou o script de outro diretório e gerou o banco em outro lugar.
Solução: usePath()como no exemplo e rode os scripts na mesma pasta.
10) Extras úteis (opcionais, mas recomendados)
- Índice para acelerar filtros por status:
CREATE INDEX IF NOT EXISTS idx_tarefas_status ON tarefas(status); - Pesquisar por termo no título (LIKE):
SELECT * FROM tarefas WHERE titulo LIKE '%' || ? || '%'; - Exportar para JSON: use
listar_tarefas()+json.dump(...). - Modo WAL (escrita concorrente melhor):
PRAGMA journal_mode=WAL;(guarde para depois; em apps simples, o padrão já atende.)
11) Boas práticas desde cedo
- Primeiro, Nunca monte SQL com concatenação de strings do usuário → use parâmetros
?. - Então, Valide na aplicação (Python) e reforce no banco (constraints).
- Além disso, Feche conexões (context manager
withajuda muito). - Ainda, Padronize datas (ISO-8601 facilita debug e integração).
- Finalmente, separe responsabilidades quando o projeto crescer (conexão, repositório/CRUD, CLI/API).
Exemplo de organização em 3 arquivos (para evoluir):
crud_sqlite/
├─ db.py # conexão + criação de tabela
├─ repo.py # funções CRUD (sem I/O de terminal)
└─ cli.py # menu e interação com o usuário (chama repo)
12) Exercícios guiados (para fixar de verdade)
- Marcar tudo como concluído: atualize todas as tarefas com
status <> 'concluida', setandostatus='concluida'eatualizado_em=AGORA. Exiba quantas linhas foram afetadas. - Busca por termo: pergunte um termo e liste tarefas cujo
titulocontenha esse termo usandoLIKEcom parâmetros. - Relatório por status: mostre quantas tarefas há por
status(useGROUP BY status). - Exportar CSV/JSON: gere
tarefas.csvoutarefas.jsoncom o resultado delistar_tarefas().
13) Próximo nível (quando se sentir pronto)
- Criar uma API Web com FastAPI reaproveitando as funções do repositório.
- Adicionar testes com
pytest(cenários: inserir/listar/atualizar/excluir). - Migrar de SQL manual para um ORM (SQLAlchemy, Peewee) quando o domínio aumentar.
14) Checklist rápido (para você se guiar)
- [ ] Conectei no SQLite e criei a tabela.
- [ ] Inserção com placeholders
?funcionando. - [ ] Lista ordenada por
idusandorow_factory. - [ ] Atualização só altera campos informados.
- [ ] Exclusão por
idconfirmandorowcount. - [ ] Validações básicas (título e status).
- [ ] Entendi transação,
commit()e quando usar. - [ ] Rodei o
app.pye brinquei no menu.
Veja também (links internos)
- Introdução ao Python: primeiros passos
- Tipos de dados e variáveis em Python
- Seu primeiro programa em Python: passo a passo
