Projeto Estoque com SQLite

A padaria da Ana controla o estoque num caderno: chegou café, risca e escreve o número novo; vendeu bolo, risca de novo. Neste projeto, o caderno vira um programa. Os produtos ficam guardados num banco de dados SQLite, um arquivo estoque.db que continua lá depois que o programa termina, e um menu no terminal permite listar, cadastrar, dar entrada, dar saída e ver um relatório. O programa recusa um produto repetido, não deixa o estoque ficar negativo e não quebra quando alguém digita "quinze" no lugar de 15.

Você vai praticar

O que vamos construir

Este é o programa pronto, no computador da Ana. O que aparece depois de cada pergunta foi digitado por ela (o 1, o Sonho, o quinze…); o resto foi o programa que escreveu:

$ python3 estoque.py

1) Listar  2) Cadastrar  3) Entrada  4) Saída  5) Relatório  0) Sair
Opção: 1
=== Estoque da Padaria Pão Quente ===
Bolo de milho           4 un.   R$  18.00
Café 500 g             10 un.   R$  18.90
Leite integral 1 L     24 un.   R$   5.50
Pão francês           120 un.   R$   0.80

1) Listar  2) Cadastrar  3) Entrada  4) Saída  5) Relatório  0) Sair
Opção: 2
Nome do produto: Sonho
Quantidade: quinze
"quinze" não é um número inteiro. Tente de novo.   « texto no lugar de número
Quantidade: 15
Preço (R$): 6,50                                   « com vírgula também vale
Cadastrado: Sonho

1) Listar  2) Cadastrar  3) Entrada  4) Saída  5) Relatório  0) Sair
Opção: 4
Produto: Bolo de milho
Quantas saíram? 6
Não dá para tirar 6 de Bolo de milho: só há 4.     « o estoque não fica negativo

1) Listar  2) Cadastrar  3) Entrada  4) Saída  5) Relatório  0) Sair
Opção: 4
Produto: Bolo de milho
Quantas saíram? 3
Bolo de milho: 4 -> 1

1) Listar  2) Cadastrar  3) Entrada  4) Saída  5) Relatório  0) Sair
Opção: 3
Produto: Café 500 g
Quantas chegaram? 12
Café 500 g: 10 -> 22

1) Listar  2) Cadastrar  3) Entrada  4) Saída  5) Relatório  0) Sair
Opção: 5
Valor do estoque: R$ 759.30
Abaixo de 5 unidades:
  - Bolo de milho (1)

1) Listar  2) Cadastrar  3) Entrada  4) Saída  5) Relatório  0) Sair
Opção: 9
Opção inválida: 9

1) Listar  2) Cadastrar  3) Entrada  4) Saída  5) Relatório  0) Sair
Opção: 0
Até amanhã!

E no dia seguinte, quando a Ana abre o programa de novo, tudo continua lá: o Sonho cadastrado, o bolo com 1 unidade, o café com 22. É a diferença entre guardar numa variável (que some quando o programa termina) e guardar num banco:

$ python3 estoque.py

1) Listar  2) Cadastrar  3) Entrada  4) Saída  5) Relatório  0) Sair
Opção: 1
=== Estoque da Padaria Pão Quente ===
Bolo de milho           1 un.   R$  18.00
Café 500 g             22 un.   R$  18.90
Leite integral 1 L     24 un.   R$   5.50
Pão francês           120 un.   R$   0.80
Sonho                  15 un.   R$   6.50

1) Listar  2) Cadastrar  3) Entrada  4) Saída  5) Relatório  0) Sair
Opção: 0
Até amanhã!

Vamos chegar lá em quatro versões. No fim de cada uma o programa roda, e cada versão acrescenta uma coisa só:

  1. O banco, a tabela e a listagem (connect, CREATE TABLE, INSERT, SELECT).
  2. Funções e o cadastro sem repetição (def, with con:, IntegrityError).
  3. Entrada e saída de mercadoria (UPDATE, sem deixar negativo).
  4. O menu e o relatório (while, match, sum(), entrada validada).

Antes de começar

Leia antes a aula Python com Banco de Dados (sqlite3): ela explica cada comando SQL usado aqui, com calma. Você também precisa saber criar e rodar um arquivo .py (Rodando um Programa). No seu computador, crie a pasta do projeto; o arquivo vai se chamar estoque.py:

$ mkdir -p projetos/estoque
$ cd projetos/estoque
No editor do guia, cada Rodar começa do zero

Nas caixas Experimente desta página, o programa cria o estoque.db e o usa de verdade, mas, como em toda caixa do guia, cada Rodar parte do zero: os arquivos da execução anterior somem. Por isso o programa cria o banco e a tabela sozinho, e coloca os produtos iniciais quando a tabela está vazia. No seu computador, o arquivo fica na pasta e os dados se acumulam de uma execução para outra, como nas sessões de terminal desta página.


Versão 1 · O banco, a tabela e a listagem

A primeira versão abre (ou cria) o banco, cria a tabela de produtos se ela ainda não existir, coloca quatro produtos se a tabela estiver vazia e mostra o estoque em colunas.

import sqlite3

con = sqlite3.connect("estoque.db")
con.execute("""
    CREATE TABLE IF NOT EXISTS produtos (
        id INTEGER PRIMARY KEY,
        nome TEXT NOT NULL UNIQUE,
        quantidade INTEGER NOT NULL,
        preco REAL NOT NULL
    )
""")

iniciais = [
    ("Pão francês", 120, 0.80),
    ("Leite integral 1 L", 24, 5.50),
    ("Café 500 g", 10, 18.90),
    ("Bolo de milho", 4, 18.00),
]

quantos = con.execute("SELECT count(*) FROM produtos").fetchone()[0]
if quantos == 0:
    con.executemany(
        "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
        iniciais,
    )
    con.commit()
    print(f"Banco novo: {len(iniciais)} produtos cadastrados.")

print("=== Estoque da Padaria Pão Quente ===")
for nome, quantidade, preco in con.execute(
    "SELECT nome, quantidade, preco FROM produtos ORDER BY nome"
):
    print(f"{nome:<20} {quantidade:>4} un.   R$ {preco:>6.2f}")

con.close()

Por partes

sqlite3.connect("estoque.db")
Abre o banco guardado no arquivo estoque.db. Se o arquivo não existe, ele é criado, vazio.
CREATE TABLE IF NOT EXISTS produtos (…)
Cria a tabela só se ela ainda não existir. Sem o IF NOT EXISTS, a segunda execução daria erro (table produtos already exists). O texto entre três aspas (""") pode ocupar várias linhas.
id INTEGER PRIMARY KEY
Um número único para cada produto, que o SQLite preenche sozinho: 1, 2, 3…
nome TEXT NOT NULL UNIQUE
O nome é um texto obrigatório (NOT NULL) e não pode se repetir (UNIQUE). Essa regra vai trabalhar a nosso favor na versão 2.
quantidade INTEGER, preco REAL
Um número inteiro e um número com casas decimais.
fetchone()[0]
O SELECT count(*) devolve uma linha com um valor só, a contagem. fetchone() pega essa linha (uma tupla) e [0] pega o valor de dentro.
executemany(…, iniciais)
Roda o mesmo INSERT uma vez para cada tupla da lista. Cada ? é preenchido por um item da tupla, na ordem: nome, quantidade, preço.
con.commit()
Grava de verdade no arquivo. Sem ele, os produtos sumiriam ao fechar o banco.
for nome, quantidade, preco in con.execute(…)
Percorre o resultado do SELECT: cada linha é uma tupla, aberta em três variáveis.
{nome:<20} {quantidade:>4} {preco:>6.2f}
Alinhamento: o nome à esquerda em 20 posições, a quantidade à direita em 4, o preço à direita em 6 com 2 casas (Formatação). É o que deixa as colunas retas.

Rodando duas vezes no computador da Ana:

$ python3 estoque.py
Banco novo: 4 produtos cadastrados.
=== Estoque da Padaria Pão Quente ===
Bolo de milho           4 un.   R$  18.00
Café 500 g             10 un.   R$  18.90
Leite integral 1 L     24 un.   R$   5.50
Pão francês           120 un.   R$   0.80
$ python3 estoque.py                               « a 2ª vez: o banco já existe
=== Estoque da Padaria Pão Quente ===
Bolo de milho           4 un.   R$  18.00
Café 500 g             10 un.   R$  18.90
Leite integral 1 L     24 un.   R$   5.50
Pão francês           120 un.   R$   0.80

Na primeira vez, a tabela estava vazia e o programa cadastrou os quatro produtos. Na segunda, a contagem deu 4, o if foi pulado e nada foi cadastrado de novo. Os produtos vieram do arquivo. Dá até para espiar o arquivo com o programa sqlite3 do terminal, sem Python nenhum:

$ sqlite3 estoque.db "SELECT * FROM produtos;"
1|Pão francês|120|0.8
2|Leite integral 1 L|24|5.5
3|Café 500 g|10|18.9
4|Bolo de milho|4|18.0

Lá estão os id que o SQLite preencheu (1 a 4), na ordem em que os produtos foram gravados.

Mudou a lista de produtos iniciais e nada aconteceu?

No seu computador, os produtos iniciais só entram quando a tabela está vazia, ou seja, na primeira vez. Se você acrescentar um produto na lista depois disso, ele não aparece: o banco já tem 4 produtos. Para recomeçar do zero, apague o arquivo: rm estoque.db. (No editor do guia isso não acontece, porque cada Rodar já começa sem o arquivo.)

Missão: a padaria começou a vender Sonho: 15 unidades a R$ 6,50. Acrescente o sonho à lista de produtos iniciais (lembre: no Python, o decimal é com ponto) e rode.

import sqlite3

con = sqlite3.connect("estoque.db")
con.execute("""
    CREATE TABLE IF NOT EXISTS produtos (
        id INTEGER PRIMARY KEY,
        nome TEXT NOT NULL UNIQUE,
        quantidade INTEGER NOT NULL,
        preco REAL NOT NULL
    )
""")

iniciais = [
    ("Pão francês", 120, 0.80),
    ("Leite integral 1 L", 24, 5.50),
    ("Café 500 g", 10, 18.90),
    ("Bolo de milho", 4, 18.00),
]

quantos = con.execute("SELECT count(*) FROM produtos").fetchone()[0]
if quantos == 0:
    con.executemany(
        "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
        iniciais,
    )
    con.commit()
    print(f"Banco novo: {len(iniciais)} produtos cadastrados.")

print("=== Estoque da Padaria Pão Quente ===")
for nome, quantidade, preco in con.execute(
    "SELECT nome, quantidade, preco FROM produtos ORDER BY nome"
):
    print(f"{nome:<20} {quantidade:>4} un.   R$ {preco:>6.2f}")

con.close()
import sqlite3

con = sqlite3.connect("estoque.db")
con.execute("""
    CREATE TABLE IF NOT EXISTS produtos (
        id INTEGER PRIMARY KEY,
        nome TEXT NOT NULL UNIQUE,
        quantidade INTEGER NOT NULL,
        preco REAL NOT NULL
    )
""")

iniciais = [
    ("Pão francês", 120, 0.80),
    ("Leite integral 1 L", 24, 5.50),
    ("Café 500 g", 10, 18.90),
    ("Bolo de milho", 4, 18.00),
    ("Sonho", 15, 6.50),
]

quantos = con.execute("SELECT count(*) FROM produtos").fetchone()[0]
if quantos == 0:
    con.executemany(
        "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
        iniciais,
    )
    con.commit()
    print(f"Banco novo: {len(iniciais)} produtos cadastrados.")

print("=== Estoque da Padaria Pão Quente ===")
for nome, quantidade, preco in con.execute(
    "SELECT nome, quantidade, preco FROM produtos ORDER BY nome"
):
    print(f"{nome:<20} {quantidade:>4} un.   R$ {preco:>6.2f}")

con.close()

Versão 2 · Funções e o cadastro sem repetição

O programa vai crescer, então é hora de separá-lo em funções, cada uma com uma tarefa: abrir_banco, cadastrar e listar. E o cadastro precisa lidar com o caso em que o produto já existe.

import sqlite3

INICIAIS = [
    ("Pão francês", 120, 0.80),
    ("Leite integral 1 L", 24, 5.50),
    ("Café 500 g", 10, 18.90),
    ("Bolo de milho", 4, 18.00),
]


def abrir_banco(arquivo):
    con = sqlite3.connect(arquivo)
    con.execute("""
        CREATE TABLE IF NOT EXISTS produtos (
            id INTEGER PRIMARY KEY,
            nome TEXT NOT NULL UNIQUE,
            quantidade INTEGER NOT NULL,
            preco REAL NOT NULL
        )
    """)
    quantos = con.execute("SELECT count(*) FROM produtos").fetchone()[0]
    if quantos == 0:
        with con:
            con.executemany(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                INICIAIS,
            )
    return con


def cadastrar(con, nome, quantidade, preco):
    try:
        with con:
            con.execute(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                (nome, quantidade, preco),
            )
    except sqlite3.IntegrityError:
        print(f"Já existe um produto chamado {nome}.")
        return False
    print(f"Cadastrado: {nome}")
    return True


def listar(con):
    print("=== Estoque da Padaria Pão Quente ===")
    for nome, quantidade, preco in con.execute(
        "SELECT nome, quantidade, preco FROM produtos ORDER BY nome"
    ):
        print(f"{nome:<20} {quantidade:>4} un.   R$ {preco:>6.2f}")


con = abrir_banco("estoque.db")
cadastrar(con, "Sonho", 15, 6.50)
cadastrar(con, "Café 500 g", 5, 19.90)
listar(con)
con.close()
INICIAIS
A lista saiu do meio do programa e foi para o topo. O nome em MAIÚSCULAS é o costume para valores fixos, que o programa só lê (aula Escopo).
def abrir_banco(arquivo): … return con
Tudo o que a versão 1 fazia para preparar o banco, agora numa função que devolve a conexão pronta.
with con:
Um bloco que faz o commit sozinho quando termina bem e desfaz tudo (rollback) se der erro no meio. É mais seguro que lembrar de chamar con.commit() (aula sqlite3).
try: … except sqlite3.IntegrityError:
Se o nome já existe, a regra UNIQUE da tabela recusa o INSERT com um IntegrityError ("erro de integridade"). O except pega esse erro e mostra um aviso, em vez de o programa quebrar.
return False / return True
A função conta a quem chamou se cadastrou ou não. Este programa ainda não usa a resposta, mas ela já está lá para quem precisar.

Rodando no computador da Ana, sobre o banco que a versão 1 criou:

$ python3 estoque.py
Cadastrado: Sonho
Já existe um produto chamado Café 500 g.
=== Estoque da Padaria Pão Quente ===
Bolo de milho           4 un.   R$  18.00
Café 500 g             10 un.   R$  18.90
Leite integral 1 L     24 un.   R$   5.50
Pão francês           120 un.   R$   0.80
Sonho                  15 un.   R$   6.50
$ python3 estoque.py
Já existe um produto chamado Sonho.                « na 2ª vez, o Sonho já estava lá
Já existe um produto chamado Café 500 g.
=== Estoque da Padaria Pão Quente ===
Bolo de milho           4 un.   R$  18.00
Café 500 g             10 un.   R$  18.90
Leite integral 1 L     24 un.   R$   5.50
Pão francês           120 un.   R$   0.80
Sonho                  15 un.   R$   6.50

Quando dá errado na versão 2

Sem o try: o nome repetido derruba o programa

Sem o try/except no cadastrar, o segundo cadastro do café para tudo:

$ python3 estoque.py
Cadastrado: Sonho
Traceback (most recent call last):
  File "/home/ana/projetos/estoque/estoque.py", line 51, in <module>
    cadastrar(con, "Café 500 g", 5, 19.90)
    ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ana/projetos/estoque/estoque.py", line 33, in cadastrar
    con.execute(
    ~~~~~~~~~~~^
        "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        (nome, quantidade, preco),
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
sqlite3.IntegrityError: UNIQUE constraint failed: produtos.nome

Leia de baixo para cima: sqlite3.IntegrityError: UNIQUE constraint failed: produtos.nome, "a regra UNIQUE falhou na coluna nome da tabela produtos". O caminho mostra que a linha 51 (o programa principal) chamou cadastrar, e dentro dele o con.execute da linha 33 foi recusado. Como o execute ocupa várias linhas, o Python marca o comando inteiro com ^. O sqlite3. antes do nome quer dizer que esse tipo de erro vem do módulo sqlite3; por isso o except escreve sqlite3.IntegrityError.

Repare que o banco não aceitou o café repetido mesmo sem o try: quem protege os dados é a regra UNIQUE da tabela. O try só faz o programa reagir com educação, em vez de quebrar.

Missão: nesta versão, a função cadastrar está sem o try, e o café repetido derruba o programa. Rode, leia o IntegrityError e proteja o with con: com try/except sqlite3.IntegrityError, mostrando Já existe um produto chamado Café 500 g. e deixando o programa seguir até a listagem.

import sqlite3

INICIAIS = [
    ("Pão francês", 120, 0.80),
    ("Leite integral 1 L", 24, 5.50),
    ("Café 500 g", 10, 18.90),
    ("Bolo de milho", 4, 18.00),
]


def abrir_banco(arquivo):
    con = sqlite3.connect(arquivo)
    con.execute("""
        CREATE TABLE IF NOT EXISTS produtos (
            id INTEGER PRIMARY KEY,
            nome TEXT NOT NULL UNIQUE,
            quantidade INTEGER NOT NULL,
            preco REAL NOT NULL
        )
    """)
    quantos = con.execute("SELECT count(*) FROM produtos").fetchone()[0]
    if quantos == 0:
        with con:
            con.executemany(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                INICIAIS,
            )
    return con


def cadastrar(con, nome, quantidade, preco):
    with con:
        con.execute(
            "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
            (nome, quantidade, preco),
        )
    print(f"Cadastrado: {nome}")
    return True


def listar(con):
    print("=== Estoque da Padaria Pão Quente ===")
    for nome, quantidade, preco in con.execute(
        "SELECT nome, quantidade, preco FROM produtos ORDER BY nome"
    ):
        print(f"{nome:<20} {quantidade:>4} un.   R$ {preco:>6.2f}")


con = abrir_banco("estoque.db")
cadastrar(con, "Sonho", 15, 6.50)
cadastrar(con, "Café 500 g", 5, 19.90)
listar(con)
con.close()
import sqlite3

INICIAIS = [
    ("Pão francês", 120, 0.80),
    ("Leite integral 1 L", 24, 5.50),
    ("Café 500 g", 10, 18.90),
    ("Bolo de milho", 4, 18.00),
]


def abrir_banco(arquivo):
    con = sqlite3.connect(arquivo)
    con.execute("""
        CREATE TABLE IF NOT EXISTS produtos (
            id INTEGER PRIMARY KEY,
            nome TEXT NOT NULL UNIQUE,
            quantidade INTEGER NOT NULL,
            preco REAL NOT NULL
        )
    """)
    quantos = con.execute("SELECT count(*) FROM produtos").fetchone()[0]
    if quantos == 0:
        with con:
            con.executemany(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                INICIAIS,
            )
    return con


def cadastrar(con, nome, quantidade, preco):
    try:
        with con:
            con.execute(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                (nome, quantidade, preco),
            )
    except sqlite3.IntegrityError:
        print(f"Já existe um produto chamado {nome}.")
        return False
    print(f"Cadastrado: {nome}")
    return True


def listar(con):
    print("=== Estoque da Padaria Pão Quente ===")
    for nome, quantidade, preco in con.execute(
        "SELECT nome, quantidade, preco FROM produtos ORDER BY nome"
    ):
        print(f"{nome:<20} {quantidade:>4} un.   R$ {preco:>6.2f}")


con = abrir_banco("estoque.db")
cadastrar(con, "Sonho", 15, 6.50)
cadastrar(con, "Café 500 g", 5, 19.90)
listar(con)
con.close()

Versão 3 · Entrada e saída de mercadoria

Chegou café, vendeu pão: a quantidade muda. Uma função só, movimentar, faz as duas coisas: um número positivo é entrada, um negativo é saída. Antes de gravar, ela confere duas coisas: se o produto existe e se a saída não deixa o estoque negativo. O resto do programa (abrir_banco, cadastrar, listar) é igual ao da versão 2; estas são as partes novas:

def movimentar(con, nome, quantidade):
    linha = con.execute(
        "SELECT quantidade FROM produtos WHERE nome = ?", (nome,)
    ).fetchone()
    if linha is None:
        print(f"Produto não encontrado: {nome}")
        return False
    atual = linha[0]
    nova = atual + quantidade
    if nova < 0:
        print(f"Não dá para tirar {-quantidade} de {nome}: só há {atual}.")
        return False
    with con:
        con.execute(
            "UPDATE produtos SET quantidade = ? WHERE nome = ?", (nova, nome)
        )
    print(f"{nome}: {atual} -> {nova}")
    return True


con = abrir_banco("estoque.db")
movimentar(con, "Pão francês", -30)
movimentar(con, "Café 500 g", 12)
movimentar(con, "Bolo de milho", -6)
movimentar(con, "Pão de mel", 5)
listar(con)
con.close()
WHERE nome = ?", (nome,)
Procura a linha do produto. Repare na vírgula em (nome,): é ela que faz uma tupla de um item só (aula Tuplas). Sem a vírgula, dá erro; veja abaixo.
if linha is None:
Quando o SELECT não acha nada, o fetchone() devolve None (aula O Valor None): o produto não existe.
nova = atual + quantidade
Com -30, 120 + (-30) dá 90. A mesma conta serve para entrada e saída.
if nova < 0:
A saída deixaria o estoque negativo: avisa e sai da função com return False, sem gravar nada. O {-quantidade} mostra o número sem o sinal de menos.
UPDATE produtos SET quantidade = ? WHERE nome = ?
Troca a quantidade daquele produto. São dois ?, então a tupla tem dois valores, na mesma ordem: (nova, nome). Sem o WHERE, o UPDATE mudaria todos os produtos.
$ python3 estoque.py
Pão francês: 120 -> 90
Café 500 g: 10 -> 22
Não dá para tirar 6 de Bolo de milho: só há 4.
Produto não encontrado: Pão de mel
=== Estoque da Padaria Pão Quente ===
Bolo de milho           4 un.   R$  18.00
Café 500 g             22 un.   R$  18.90
Leite integral 1 L     24 un.   R$   5.50
Pão francês            90 un.   R$   0.80
Sonho                  15 un.   R$   6.50

Quando dá errado na versão 3

Sem a conferência, o estoque fica negativo

Sem o if nova < 0, o programa obedece cegamente, começando com um banco novo:

$ python3 estoque.py
Pão francês: 120 -> 90
Café 500 g: 10 -> 22
Bolo de milho: 4 -> -2                             « vendeu bolo que não existia
Produto não encontrado: Pão de mel
=== Estoque da Padaria Pão Quente ===
Bolo de milho          -2 un.   R$  18.00
Café 500 g             22 un.   R$  18.90
Leite integral 1 L     24 un.   R$   5.50
Pão francês            90 un.   R$   0.80

Nenhum erro, só um estoque impossível: menos dois bolos. O banco não sabe que quantidade negativa não faz sentido; quem tem de conferir é o programa.

Esqueceu o with con: (e o commit)

Se o UPDATE for feito com con.execute(…) solto, sem with con: nem con.commit(), tudo parece funcionar. Mas rode duas vezes, com um banco novo:

$ python3 estoque.py
Pão francês: 120 -> 90
Café 500 g: 10 -> 22
Não dá para tirar 6 de Bolo de milho: só há 4.
Produto não encontrado: Pão de mel
=== Estoque da Padaria Pão Quente ===
Bolo de milho           4 un.   R$  18.00
Café 500 g             22 un.   R$  18.90
Leite integral 1 L     24 un.   R$   5.50
Pão francês            90 un.   R$   0.80
$ python3 estoque.py
Pão francês: 120 -> 90                             « de novo 120: a venda de ontem sumiu
Café 500 g: 10 -> 22
Não dá para tirar 6 de Bolo de milho: só há 4.
Produto não encontrado: Pão de mel
=== Estoque da Padaria Pão Quente ===
Bolo de milho           4 un.   R$  18.00
Café 500 g             22 un.   R$  18.90
Leite integral 1 L     24 un.   R$   5.50
Pão francês            90 un.   R$   0.80

Dentro de cada execução, a listagem mostra 90 pães. Mas a segunda execução começa de novo com 120: as mudanças ficaram "a lápis" e foram jogadas fora no con.close(). Sem mensagem de erro nenhuma. Como consertar: toda mudança (INSERT, UPDATE, DELETE) dentro de um with con:.

Esqueceu a vírgula da tupla
import sqlite3

con = sqlite3.connect("estoque.db")
nome = "Café 500 g"
linha = con.execute("SELECT quantidade FROM produtos WHERE nome = ?", (nome)).fetchone()
print(linha)
$ python3 consulta.py
Traceback (most recent call last):
  File "/home/ana/projetos/estoque/consulta.py", line 5, in <module>
    linha = con.execute("SELECT quantidade FROM produtos WHERE nome = ?", (nome)).fetchone()
            ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
sqlite3.ProgrammingError: Incorrect number of bindings supplied. The current statement uses 1, and there are 10 supplied.

"Número errado de valores: o comando usa 1, e foram fornecidos 10". Dez? Sem a vírgula, (nome) não é uma tupla, é só o texto "Café 500 g" entre parênteses. O sqlite3 então tratou o texto como uma sequência de letras: são 10 caracteres, contando os espaços. Como consertar: (nome,), com a vírgula.

Montar o SQL com f-string

Parece mais fácil escrever o nome direto no comando com uma f-string. Até aparecer um produto com apóstrofo:

import sqlite3

con = sqlite3.connect("estoque.db")
nome = "Biscoito D'Ouro"
linha = con.execute(f"SELECT quantidade FROM produtos WHERE nome = '{nome}'").fetchone()
print(linha)
$ python3 consulta.py
Traceback (most recent call last):
  File "/home/ana/projetos/estoque/consulta.py", line 5, in <module>
    linha = con.execute(f"SELECT quantidade FROM produtos WHERE nome = '{nome}'").fetchone()
            ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
sqlite3.OperationalError: near "Ouro": syntax error

O comando montado ficou … WHERE nome = 'Biscoito D'Ouro': o apóstrofo fechou o texto em 'Biscoito D', e o banco tropeçou no Ouro que sobrou. Pior que esse erro é quando alguém digita de propósito um texto que muda o comando: é o SQL injection, explicado na aula sqlite3. Com o ?, o mesmo nome funciona (o None só diz que esse biscoito não está cadastrado):

import sqlite3

con = sqlite3.connect("estoque.db")
nome = "Biscoito D'Ouro"
linha = con.execute("SELECT quantidade FROM produtos WHERE nome = ?", (nome,)).fetchone()
print(linha)
$ python3 consulta.py
None

Regra deste projeto: valor que vem de variável entra no SQL sempre por um ?, nunca por f-string.

Nome de tabela errado
import sqlite3

con = sqlite3.connect("estoque.db")
print(con.execute("SELECT nome FROM produto").fetchall())
$ python3 consulta.py
Traceback (most recent call last):
  File "/home/ana/projetos/estoque/consulta.py", line 4, in <module>
    print(con.execute("SELECT nome FROM produto").fetchall())
          ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
sqlite3.OperationalError: no such table: produto

"Não existe a tabela produto": a tabela se chama produtos, no plural. O Python não confere o texto do SQL; quem descobre o erro é o SQLite, na hora de rodar.

Missão: nesta versão falta a conferência, e o bolo fica com -2. Acrescente na função movimentar, antes do with con:, o teste que recusa a saída quando o estoque ficaria negativo, com a mensagem Não dá para tirar 6 de Bolo de milho: só há 4.

import sqlite3

INICIAIS = [
    ("Pão francês", 120, 0.80),
    ("Leite integral 1 L", 24, 5.50),
    ("Café 500 g", 10, 18.90),
    ("Bolo de milho", 4, 18.00),
]


def abrir_banco(arquivo):
    con = sqlite3.connect(arquivo)
    con.execute("""
        CREATE TABLE IF NOT EXISTS produtos (
            id INTEGER PRIMARY KEY,
            nome TEXT NOT NULL UNIQUE,
            quantidade INTEGER NOT NULL,
            preco REAL NOT NULL
        )
    """)
    quantos = con.execute("SELECT count(*) FROM produtos").fetchone()[0]
    if quantos == 0:
        with con:
            con.executemany(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                INICIAIS,
            )
    return con


def cadastrar(con, nome, quantidade, preco):
    try:
        with con:
            con.execute(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                (nome, quantidade, preco),
            )
    except sqlite3.IntegrityError:
        print(f"Já existe um produto chamado {nome}.")
        return False
    print(f"Cadastrado: {nome}")
    return True


def listar(con):
    print("=== Estoque da Padaria Pão Quente ===")
    for nome, quantidade, preco in con.execute(
        "SELECT nome, quantidade, preco FROM produtos ORDER BY nome"
    ):
        print(f"{nome:<20} {quantidade:>4} un.   R$ {preco:>6.2f}")


def movimentar(con, nome, quantidade):
    linha = con.execute(
        "SELECT quantidade FROM produtos WHERE nome = ?", (nome,)
    ).fetchone()
    if linha is None:
        print(f"Produto não encontrado: {nome}")
        return False
    atual = linha[0]
    nova = atual + quantidade
    with con:
        con.execute(
            "UPDATE produtos SET quantidade = ? WHERE nome = ?", (nova, nome)
        )
    print(f"{nome}: {atual} -> {nova}")
    return True


con = abrir_banco("estoque.db")
movimentar(con, "Pão francês", -30)
movimentar(con, "Café 500 g", 12)
movimentar(con, "Bolo de milho", -6)
movimentar(con, "Pão de mel", 5)
listar(con)
con.close()
import sqlite3

INICIAIS = [
    ("Pão francês", 120, 0.80),
    ("Leite integral 1 L", 24, 5.50),
    ("Café 500 g", 10, 18.90),
    ("Bolo de milho", 4, 18.00),
]


def abrir_banco(arquivo):
    con = sqlite3.connect(arquivo)
    con.execute("""
        CREATE TABLE IF NOT EXISTS produtos (
            id INTEGER PRIMARY KEY,
            nome TEXT NOT NULL UNIQUE,
            quantidade INTEGER NOT NULL,
            preco REAL NOT NULL
        )
    """)
    quantos = con.execute("SELECT count(*) FROM produtos").fetchone()[0]
    if quantos == 0:
        with con:
            con.executemany(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                INICIAIS,
            )
    return con


def cadastrar(con, nome, quantidade, preco):
    try:
        with con:
            con.execute(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                (nome, quantidade, preco),
            )
    except sqlite3.IntegrityError:
        print(f"Já existe um produto chamado {nome}.")
        return False
    print(f"Cadastrado: {nome}")
    return True


def listar(con):
    print("=== Estoque da Padaria Pão Quente ===")
    for nome, quantidade, preco in con.execute(
        "SELECT nome, quantidade, preco FROM produtos ORDER BY nome"
    ):
        print(f"{nome:<20} {quantidade:>4} un.   R$ {preco:>6.2f}")


def movimentar(con, nome, quantidade):
    linha = con.execute(
        "SELECT quantidade FROM produtos WHERE nome = ?", (nome,)
    ).fetchone()
    if linha is None:
        print(f"Produto não encontrado: {nome}")
        return False
    atual = linha[0]
    nova = atual + quantidade
    if nova < 0:
        print(f"Não dá para tirar {-quantidade} de {nome}: só há {atual}.")
        return False
    with con:
        con.execute(
            "UPDATE produtos SET quantidade = ? WHERE nome = ?", (nova, nome)
        )
    print(f"{nome}: {atual} -> {nova}")
    return True


con = abrir_banco("estoque.db")
movimentar(con, "Pão francês", -30)
movimentar(con, "Café 500 g", 12)
movimentar(con, "Bolo de milho", -6)
movimentar(con, "Pão de mel", 5)
listar(con)
con.close()

Versão 4 · O menu e o relatório

Até aqui, as movimentações estavam escritas no próprio programa. Na versão final, a Ana escolhe o que fazer num menu, que se repete até ela digitar 0. Entram também o relatório (o valor total do estoque e os produtos acabando) e duas funções que perguntam um número até a resposta ser válida. Este é o programa completo:

import sqlite3

INICIAIS = [
    ("Pão francês", 120, 0.80),
    ("Leite integral 1 L", 24, 5.50),
    ("Café 500 g", 10, 18.90),
    ("Bolo de milho", 4, 18.00),
]
MINIMO = 5


def abrir_banco(arquivo):
    con = sqlite3.connect(arquivo)
    con.execute("""
        CREATE TABLE IF NOT EXISTS produtos (
            id INTEGER PRIMARY KEY,
            nome TEXT NOT NULL UNIQUE,
            quantidade INTEGER NOT NULL,
            preco REAL NOT NULL
        )
    """)
    quantos = con.execute("SELECT count(*) FROM produtos").fetchone()[0]
    if quantos == 0:
        with con:
            con.executemany(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                INICIAIS,
            )
    return con


def cadastrar(con, nome, quantidade, preco):
    try:
        with con:
            con.execute(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                (nome, quantidade, preco),
            )
    except sqlite3.IntegrityError:
        print(f"Já existe um produto chamado {nome}.")
        return False
    print(f"Cadastrado: {nome}")
    return True


def listar(con):
    print("=== Estoque da Padaria Pão Quente ===")
    for nome, quantidade, preco in con.execute(
        "SELECT nome, quantidade, preco FROM produtos ORDER BY nome"
    ):
        print(f"{nome:<20} {quantidade:>4} un.   R$ {preco:>6.2f}")


def movimentar(con, nome, quantidade):
    linha = con.execute(
        "SELECT quantidade FROM produtos WHERE nome = ?", (nome,)
    ).fetchone()
    if linha is None:
        print(f"Produto não encontrado: {nome}")
        return False
    atual = linha[0]
    nova = atual + quantidade
    if nova < 0:
        print(f"Não dá para tirar {-quantidade} de {nome}: só há {atual}.")
        return False
    with con:
        con.execute(
            "UPDATE produtos SET quantidade = ? WHERE nome = ?", (nova, nome)
        )
    print(f"{nome}: {atual} -> {nova}")
    return True


def relatorio(con):
    total = con.execute(
        "SELECT sum(quantidade * preco) FROM produtos"
    ).fetchone()[0]
    print(f"Valor do estoque: R$ {total:.2f}")
    acabando = con.execute(
        "SELECT nome, quantidade FROM produtos WHERE quantidade < ? ORDER BY quantidade",
        (MINIMO,),
    ).fetchall()
    if acabando:
        print(f"Abaixo de {MINIMO} unidades:")
        for nome, quantidade in acabando:
            print(f"  - {nome} ({quantidade})")
    else:
        print("Nenhum produto acabando.")


def ler_inteiro(pergunta):
    while True:
        texto = input(pergunta).strip()
        try:
            return int(texto)
        except ValueError:
            print(f'"{texto}" não é um número inteiro. Tente de novo.')


def ler_preco(pergunta):
    while True:
        texto = input(pergunta).strip().replace(",", ".")
        try:
            return float(texto)
        except ValueError:
            print(f'"{texto}" não é um preço. Tente de novo.')


def menu(con):
    while True:
        print()
        print("1) Listar  2) Cadastrar  3) Entrada  4) Saída  5) Relatório  0) Sair")
        opcao = input("Opção: ").strip()
        match opcao:
            case "1":
                listar(con)
            case "2":
                nome = input("Nome do produto: ").strip()
                quantidade = ler_inteiro("Quantidade: ")
                preco = ler_preco("Preço (R$): ")
                cadastrar(con, nome, quantidade, preco)
            case "3":
                nome = input("Produto: ").strip()
                movimentar(con, nome, ler_inteiro("Quantas chegaram? "))
            case "4":
                nome = input("Produto: ").strip()
                movimentar(con, nome, -ler_inteiro("Quantas saíram? "))
            case "5":
                relatorio(con)
            case "0":
                print("Até amanhã!")
                break
            case _:
                print(f"Opção inválida: {opcao}")


con = abrir_banco("estoque.db")
menu(con)
con.close()

As partes novas, uma por uma

MINIMO = 5
Abaixo de quantas unidades um produto aparece como "acabando". Fica no topo, com nome, para ser fácil de mudar.
SELECT sum(quantidade * preco)
O próprio banco multiplica a quantidade pelo preço de cada produto e soma tudo: o valor do que está na prateleira.
WHERE quantidade < ? … (MINIMO,)
Os produtos abaixo do mínimo, do que tem menos para o que tem mais (ORDER BY quantidade). O fetchall() devolve uma lista de tuplas; lista vazia conta como falso no if acabando: (aula Booleanos).
ler_inteiro e ler_preco
Perguntam até receber um número válido: o while True repete, e o return dentro do try sai da função assim que a conversão dá certo. O ler_preco troca a vírgula por ponto, para aceitar 6,50. É a mesma técnica da versão 4 do Projeto Calculadora.
-ler_inteiro("Quantas saíram? ")
Na saída, o número digitado vira negativo antes de ir para o movimentar: a Ana digita 3, a função recebe -3.
match opcao: / case "1": … case _:
Escolhe o que fazer pela opção digitada (um texto). O case _ pega qualquer outra coisa (aula match e case).
case "0": … break
Sai do while True; depois dele, o programa fecha o banco e termina.

A sessão completa está no começo da página, em O que vamos construir. O valor do relatório confere: 1 × 18,00 + 22 × 18,90 + 24 × 5,50 + 120 × 0,80 + 15 × 6,50 = R$ 759,30.

Missão: a função relatorio já está escrita, mas a opção 5 do menu responde "Opção inválida". Ligue a opção 5 à função, no match. A caixa vai digitar 5 e depois 0; o relatório do estoque inicial deve mostrar R$ 489.00 e o bolo de milho acabando.

import sqlite3

INICIAIS = [
    ("Pão francês", 120, 0.80),
    ("Leite integral 1 L", 24, 5.50),
    ("Café 500 g", 10, 18.90),
    ("Bolo de milho", 4, 18.00),
]
MINIMO = 5


def abrir_banco(arquivo):
    con = sqlite3.connect(arquivo)
    con.execute("""
        CREATE TABLE IF NOT EXISTS produtos (
            id INTEGER PRIMARY KEY,
            nome TEXT NOT NULL UNIQUE,
            quantidade INTEGER NOT NULL,
            preco REAL NOT NULL
        )
    """)
    quantos = con.execute("SELECT count(*) FROM produtos").fetchone()[0]
    if quantos == 0:
        with con:
            con.executemany(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                INICIAIS,
            )
    return con


def cadastrar(con, nome, quantidade, preco):
    try:
        with con:
            con.execute(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                (nome, quantidade, preco),
            )
    except sqlite3.IntegrityError:
        print(f"Já existe um produto chamado {nome}.")
        return False
    print(f"Cadastrado: {nome}")
    return True


def listar(con):
    print("=== Estoque da Padaria Pão Quente ===")
    for nome, quantidade, preco in con.execute(
        "SELECT nome, quantidade, preco FROM produtos ORDER BY nome"
    ):
        print(f"{nome:<20} {quantidade:>4} un.   R$ {preco:>6.2f}")


def movimentar(con, nome, quantidade):
    linha = con.execute(
        "SELECT quantidade FROM produtos WHERE nome = ?", (nome,)
    ).fetchone()
    if linha is None:
        print(f"Produto não encontrado: {nome}")
        return False
    atual = linha[0]
    nova = atual + quantidade
    if nova < 0:
        print(f"Não dá para tirar {-quantidade} de {nome}: só há {atual}.")
        return False
    with con:
        con.execute(
            "UPDATE produtos SET quantidade = ? WHERE nome = ?", (nova, nome)
        )
    print(f"{nome}: {atual} -> {nova}")
    return True


def relatorio(con):
    total = con.execute(
        "SELECT sum(quantidade * preco) FROM produtos"
    ).fetchone()[0]
    print(f"Valor do estoque: R$ {total:.2f}")
    acabando = con.execute(
        "SELECT nome, quantidade FROM produtos WHERE quantidade < ? ORDER BY quantidade",
        (MINIMO,),
    ).fetchall()
    if acabando:
        print(f"Abaixo de {MINIMO} unidades:")
        for nome, quantidade in acabando:
            print(f"  - {nome} ({quantidade})")
    else:
        print("Nenhum produto acabando.")


def ler_inteiro(pergunta):
    while True:
        texto = input(pergunta).strip()
        try:
            return int(texto)
        except ValueError:
            print(f'"{texto}" não é um número inteiro. Tente de novo.')


def ler_preco(pergunta):
    while True:
        texto = input(pergunta).strip().replace(",", ".")
        try:
            return float(texto)
        except ValueError:
            print(f'"{texto}" não é um preço. Tente de novo.')


def menu(con):
    while True:
        print()
        print("1) Listar  2) Cadastrar  3) Entrada  4) Saída  5) Relatório  0) Sair")
        opcao = input("Opção: ").strip()
        match opcao:
            case "1":
                listar(con)
            case "2":
                nome = input("Nome do produto: ").strip()
                quantidade = ler_inteiro("Quantidade: ")
                preco = ler_preco("Preço (R$): ")
                cadastrar(con, nome, quantidade, preco)
            case "3":
                nome = input("Produto: ").strip()
                movimentar(con, nome, ler_inteiro("Quantas chegaram? "))
            case "4":
                nome = input("Produto: ").strip()
                movimentar(con, nome, -ler_inteiro("Quantas saíram? "))
            case "0":
                print("Até amanhã!")
                break
            case _:
                print(f"Opção inválida: {opcao}")


con = abrir_banco("estoque.db")
menu(con)
con.close()
import sqlite3

INICIAIS = [
    ("Pão francês", 120, 0.80),
    ("Leite integral 1 L", 24, 5.50),
    ("Café 500 g", 10, 18.90),
    ("Bolo de milho", 4, 18.00),
]
MINIMO = 5


def abrir_banco(arquivo):
    con = sqlite3.connect(arquivo)
    con.execute("""
        CREATE TABLE IF NOT EXISTS produtos (
            id INTEGER PRIMARY KEY,
            nome TEXT NOT NULL UNIQUE,
            quantidade INTEGER NOT NULL,
            preco REAL NOT NULL
        )
    """)
    quantos = con.execute("SELECT count(*) FROM produtos").fetchone()[0]
    if quantos == 0:
        with con:
            con.executemany(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                INICIAIS,
            )
    return con


def cadastrar(con, nome, quantidade, preco):
    try:
        with con:
            con.execute(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                (nome, quantidade, preco),
            )
    except sqlite3.IntegrityError:
        print(f"Já existe um produto chamado {nome}.")
        return False
    print(f"Cadastrado: {nome}")
    return True


def listar(con):
    print("=== Estoque da Padaria Pão Quente ===")
    for nome, quantidade, preco in con.execute(
        "SELECT nome, quantidade, preco FROM produtos ORDER BY nome"
    ):
        print(f"{nome:<20} {quantidade:>4} un.   R$ {preco:>6.2f}")


def movimentar(con, nome, quantidade):
    linha = con.execute(
        "SELECT quantidade FROM produtos WHERE nome = ?", (nome,)
    ).fetchone()
    if linha is None:
        print(f"Produto não encontrado: {nome}")
        return False
    atual = linha[0]
    nova = atual + quantidade
    if nova < 0:
        print(f"Não dá para tirar {-quantidade} de {nome}: só há {atual}.")
        return False
    with con:
        con.execute(
            "UPDATE produtos SET quantidade = ? WHERE nome = ?", (nova, nome)
        )
    print(f"{nome}: {atual} -> {nova}")
    return True


def relatorio(con):
    total = con.execute(
        "SELECT sum(quantidade * preco) FROM produtos"
    ).fetchone()[0]
    print(f"Valor do estoque: R$ {total:.2f}")
    acabando = con.execute(
        "SELECT nome, quantidade FROM produtos WHERE quantidade < ? ORDER BY quantidade",
        (MINIMO,),
    ).fetchall()
    if acabando:
        print(f"Abaixo de {MINIMO} unidades:")
        for nome, quantidade in acabando:
            print(f"  - {nome} ({quantidade})")
    else:
        print("Nenhum produto acabando.")


def ler_inteiro(pergunta):
    while True:
        texto = input(pergunta).strip()
        try:
            return int(texto)
        except ValueError:
            print(f'"{texto}" não é um número inteiro. Tente de novo.')


def ler_preco(pergunta):
    while True:
        texto = input(pergunta).strip().replace(",", ".")
        try:
            return float(texto)
        except ValueError:
            print(f'"{texto}" não é um preço. Tente de novo.')


def menu(con):
    while True:
        print()
        print("1) Listar  2) Cadastrar  3) Entrada  4) Saída  5) Relatório  0) Sair")
        opcao = input("Opção: ").strip()
        match opcao:
            case "1":
                listar(con)
            case "2":
                nome = input("Nome do produto: ").strip()
                quantidade = ler_inteiro("Quantidade: ")
                preco = ler_preco("Preço (R$): ")
                cadastrar(con, nome, quantidade, preco)
            case "3":
                nome = input("Produto: ").strip()
                movimentar(con, nome, ler_inteiro("Quantas chegaram? "))
            case "4":
                nome = input("Produto: ").strip()
                movimentar(con, nome, -ler_inteiro("Quantas saíram? "))
            case "5":
                relatorio(con)
            case "0":
                print("Até amanhã!")
                break
            case _:
                print(f"Opção inválida: {opcao}")


con = abrir_banco("estoque.db")
menu(con)
con.close()

Formas diferentes de confirmar as mudanças

O projeto usou con.commit() na versão 1 e with con: a partir da versão 2. As duas gravam; a diferença está no que acontece quando dá erro no meio.

FormaQuando usar
con.execute(…) + con.commit()Funciona, mas depende de você lembrar do commit em todo lugar que muda dados. Um esquecimento e a mudança some, sem aviso
with con: + con.execute(…)O preferido: grava sozinho no fim do bloco e, se algo der errado no meio, desfaz tudo do bloco
executemany(…, lista)Para gravar várias linhas com o mesmo comando, como os produtos iniciais

Desafio extra: remover um produto

A padaria parou de vender o bolo de milho. Acrescente a opção 6) Remover, com uma função que apaga o produto com DELETE. Para saber se apagou alguma coisa, use o rowcount ("contagem de linhas") do resultado do execute: é o número de linhas que o comando mudou, 0 quando o produto não existe.

$ python3 estoque.py

1) Listar 2) Cadastrar 3) Entrada 4) Saída 5) Relatório 6) Remover 0) Sair
Opção: 6
Produto: Sonho
Produto não encontrado: Sonho

1) Listar 2) Cadastrar 3) Entrada 4) Saída 5) Relatório 6) Remover 0) Sair
Opção: 6
Produto: Bolo de milho
Removido: Bolo de milho

1) Listar 2) Cadastrar 3) Entrada 4) Saída 5) Relatório 6) Remover 0) Sair
Opção: 5
Valor do estoque: R$ 417.00
Nenhum produto acabando.

1) Listar 2) Cadastrar 3) Entrada 4) Saída 5) Relatório 6) Remover 0) Sair
Opção: 0
Até amanhã!

(Essa sessão começou com um banco novo, só com os quatro produtos iniciais; por isso o Sonho não foi encontrado.)

Missão: escreva a função remover(con, nome) com DELETE FROM produtos WHERE nome = ? dentro de um with con:, mostrando Removido: … ou Produto não encontrado: …, e ligue-a à opção 6 do menu. A caixa vai remover o bolo de milho, tentar remover o pão de mel, listar e sair.

import sqlite3

INICIAIS = [
    ("Pão francês", 120, 0.80),
    ("Leite integral 1 L", 24, 5.50),
    ("Café 500 g", 10, 18.90),
    ("Bolo de milho", 4, 18.00),
]
MINIMO = 5


def abrir_banco(arquivo):
    con = sqlite3.connect(arquivo)
    con.execute("""
        CREATE TABLE IF NOT EXISTS produtos (
            id INTEGER PRIMARY KEY,
            nome TEXT NOT NULL UNIQUE,
            quantidade INTEGER NOT NULL,
            preco REAL NOT NULL
        )
    """)
    quantos = con.execute("SELECT count(*) FROM produtos").fetchone()[0]
    if quantos == 0:
        with con:
            con.executemany(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                INICIAIS,
            )
    return con


def cadastrar(con, nome, quantidade, preco):
    try:
        with con:
            con.execute(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                (nome, quantidade, preco),
            )
    except sqlite3.IntegrityError:
        print(f"Já existe um produto chamado {nome}.")
        return False
    print(f"Cadastrado: {nome}")
    return True


def listar(con):
    print("=== Estoque da Padaria Pão Quente ===")
    for nome, quantidade, preco in con.execute(
        "SELECT nome, quantidade, preco FROM produtos ORDER BY nome"
    ):
        print(f"{nome:<20} {quantidade:>4} un.   R$ {preco:>6.2f}")


def movimentar(con, nome, quantidade):
    linha = con.execute(
        "SELECT quantidade FROM produtos WHERE nome = ?", (nome,)
    ).fetchone()
    if linha is None:
        print(f"Produto não encontrado: {nome}")
        return False
    atual = linha[0]
    nova = atual + quantidade
    if nova < 0:
        print(f"Não dá para tirar {-quantidade} de {nome}: só há {atual}.")
        return False
    with con:
        con.execute(
            "UPDATE produtos SET quantidade = ? WHERE nome = ?", (nova, nome)
        )
    print(f"{nome}: {atual} -> {nova}")
    return True


def relatorio(con):
    total = con.execute(
        "SELECT sum(quantidade * preco) FROM produtos"
    ).fetchone()[0]
    print(f"Valor do estoque: R$ {total:.2f}")
    acabando = con.execute(
        "SELECT nome, quantidade FROM produtos WHERE quantidade < ? ORDER BY quantidade",
        (MINIMO,),
    ).fetchall()
    if acabando:
        print(f"Abaixo de {MINIMO} unidades:")
        for nome, quantidade in acabando:
            print(f"  - {nome} ({quantidade})")
    else:
        print("Nenhum produto acabando.")


def ler_inteiro(pergunta):
    while True:
        texto = input(pergunta).strip()
        try:
            return int(texto)
        except ValueError:
            print(f'"{texto}" não é um número inteiro. Tente de novo.')


def ler_preco(pergunta):
    while True:
        texto = input(pergunta).strip().replace(",", ".")
        try:
            return float(texto)
        except ValueError:
            print(f'"{texto}" não é um preço. Tente de novo.')


def menu(con):
    while True:
        print()
        print("1) Listar  2) Cadastrar  3) Entrada  4) Saída  5) Relatório  0) Sair")
        opcao = input("Opção: ").strip()
        match opcao:
            case "1":
                listar(con)
            case "2":
                nome = input("Nome do produto: ").strip()
                quantidade = ler_inteiro("Quantidade: ")
                preco = ler_preco("Preço (R$): ")
                cadastrar(con, nome, quantidade, preco)
            case "3":
                nome = input("Produto: ").strip()
                movimentar(con, nome, ler_inteiro("Quantas chegaram? "))
            case "4":
                nome = input("Produto: ").strip()
                movimentar(con, nome, -ler_inteiro("Quantas saíram? "))
            case "5":
                relatorio(con)
            case "0":
                print("Até amanhã!")
                break
            case _:
                print(f"Opção inválida: {opcao}")


con = abrir_banco("estoque.db")
menu(con)
con.close()
import sqlite3

INICIAIS = [
    ("Pão francês", 120, 0.80),
    ("Leite integral 1 L", 24, 5.50),
    ("Café 500 g", 10, 18.90),
    ("Bolo de milho", 4, 18.00),
]
MINIMO = 5


def abrir_banco(arquivo):
    con = sqlite3.connect(arquivo)
    con.execute("""
        CREATE TABLE IF NOT EXISTS produtos (
            id INTEGER PRIMARY KEY,
            nome TEXT NOT NULL UNIQUE,
            quantidade INTEGER NOT NULL,
            preco REAL NOT NULL
        )
    """)
    quantos = con.execute("SELECT count(*) FROM produtos").fetchone()[0]
    if quantos == 0:
        with con:
            con.executemany(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                INICIAIS,
            )
    return con


def cadastrar(con, nome, quantidade, preco):
    try:
        with con:
            con.execute(
                "INSERT INTO produtos (nome, quantidade, preco) VALUES (?, ?, ?)",
                (nome, quantidade, preco),
            )
    except sqlite3.IntegrityError:
        print(f"Já existe um produto chamado {nome}.")
        return False
    print(f"Cadastrado: {nome}")
    return True


def listar(con):
    print("=== Estoque da Padaria Pão Quente ===")
    for nome, quantidade, preco in con.execute(
        "SELECT nome, quantidade, preco FROM produtos ORDER BY nome"
    ):
        print(f"{nome:<20} {quantidade:>4} un.   R$ {preco:>6.2f}")


def movimentar(con, nome, quantidade):
    linha = con.execute(
        "SELECT quantidade FROM produtos WHERE nome = ?", (nome,)
    ).fetchone()
    if linha is None:
        print(f"Produto não encontrado: {nome}")
        return False
    atual = linha[0]
    nova = atual + quantidade
    if nova < 0:
        print(f"Não dá para tirar {-quantidade} de {nome}: só há {atual}.")
        return False
    with con:
        con.execute(
            "UPDATE produtos SET quantidade = ? WHERE nome = ?", (nova, nome)
        )
    print(f"{nome}: {atual} -> {nova}")
    return True


def relatorio(con):
    total = con.execute(
        "SELECT sum(quantidade * preco) FROM produtos"
    ).fetchone()[0]
    print(f"Valor do estoque: R$ {total:.2f}")
    acabando = con.execute(
        "SELECT nome, quantidade FROM produtos WHERE quantidade < ? ORDER BY quantidade",
        (MINIMO,),
    ).fetchall()
    if acabando:
        print(f"Abaixo de {MINIMO} unidades:")
        for nome, quantidade in acabando:
            print(f"  - {nome} ({quantidade})")
    else:
        print("Nenhum produto acabando.")


def remover(con, nome):
    with con:
        cursor = con.execute("DELETE FROM produtos WHERE nome = ?", (nome,))
    if cursor.rowcount == 0:
        print(f"Produto não encontrado: {nome}")
    else:
        print(f"Removido: {nome}")


def ler_inteiro(pergunta):
    while True:
        texto = input(pergunta).strip()
        try:
            return int(texto)
        except ValueError:
            print(f'"{texto}" não é um número inteiro. Tente de novo.')


def ler_preco(pergunta):
    while True:
        texto = input(pergunta).strip().replace(",", ".")
        try:
            return float(texto)
        except ValueError:
            print(f'"{texto}" não é um preço. Tente de novo.')


def menu(con):
    while True:
        print()
        print("1) Listar 2) Cadastrar 3) Entrada 4) Saída 5) Relatório 6) Remover 0) Sair")
        opcao = input("Opção: ").strip()
        match opcao:
            case "1":
                listar(con)
            case "2":
                nome = input("Nome do produto: ").strip()
                quantidade = ler_inteiro("Quantidade: ")
                preco = ler_preco("Preço (R$): ")
                cadastrar(con, nome, quantidade, preco)
            case "3":
                nome = input("Produto: ").strip()
                movimentar(con, nome, ler_inteiro("Quantas chegaram? "))
            case "4":
                nome = input("Produto: ").strip()
                movimentar(con, nome, -ler_inteiro("Quantas saíram? "))
            case "5":
                relatorio(con)
            case "6":
                remover(con, input("Produto: ").strip())
            case "0":
                print("Até amanhã!")
                break
            case _:
                print(f"Opção inválida: {opcao}")


con = abrir_banco("estoque.db")
menu(con)
con.close()

Referência rápida do projeto

EscritaPara que serviu
sqlite3.connect("estoque.db")Abrir (ou criar) o arquivo do banco
CREATE TABLE IF NOT EXISTS …Criar a tabela só na primeira vez
nome TEXT NOT NULL UNIQUEProibir nome vazio e nome repetido
INSERT … VALUES (?, ?, ?), (a, b, c)Gravar uma linha; cada ? recebe um valor da tupla, na ordem
executemany(sql, lista)Gravar várias linhas de uma vez
SELECT … ORDER BY nome + for a, b, c in …Listar, já em ordem
fetchone() / fetchall()Uma linha (ou None) / a lista de todas
UPDATE … SET quantidade = ? WHERE nome = ?Mudar a quantidade de um produto
DELETE … WHERE nome = ? + rowcountApagar e saber se apagou
sum(quantidade * preco)Somar no próprio banco
with con:Gravar no fim do bloco, ou desfazer tudo se der erro
except sqlite3.IntegrityError:Reagir ao nome repetido
SintomaCausa provável
sqlite3.IntegrityError: UNIQUE constraint failed: produtos.nomeCadastro de um nome que já existe, sem try
Estoque negativo, sem erroFaltou conferir a saída antes do UPDATE
A mudança some na execução seguinteFaltou with con: (ou con.commit())
Incorrect number of bindings suppliedA quantidade de valores não bate com a de ?; com um valor só, faltou a vírgula: (nome,)
OperationalError: near "…": syntax errorSQL montado com f-string quebrou num apóstrofo: use ?
OperationalError: no such table: …Nome da tabela digitado errado
Produto novo na lista inicial não aparece (no seu computador)A tabela já tinha produtos: apague o estoque.db para recomeçar

Exercícios

1. O marcador

Complete o comando com o marcador que recebe o nome:

con.execute("SELECT preco FROM produtos WHERE nome = ", (nome,))

2. A tupla de um item

Complete o que falta para (nome) virar uma tupla de um item:

(nome)

3. Só na primeira vez

Complete para que a tabela seja criada só se ainda não existir:

CREATE TABLE IF produtos (…)

4. A Ana roda a versão 2 duas vezes no computador dela. Por que, na segunda vez, aparece "Já existe um produto chamado Sonho."?

5. O que acontece com um UPDATE feito dentro de with con: quando dá um erro no meio do bloco?

6. Por que o projeto nunca monta o SQL com f-string, como f"… WHERE nome = '{nome}'"?


Resumo

  • O banco SQLite é um arquivo (estoque.db): os dados continuam lá depois que o programa termina. No editor do guia, cada Rodar começa sem ele; por isso o programa cria a tabela e os produtos iniciais sozinho.
  • CREATE TABLE IF NOT EXISTS e "só cadastra os iniciais se a tabela está vazia" deixam o programa rodar quantas vezes for preciso.
  • Todo valor que vem de variável entra no SQL por um ?, com uma tupla na mesma ordem; com um valor só, (nome,).
  • Toda mudança vai dentro de with con:: grava no fim, desfaz se der erro. Sem isso, a mudança some sem aviso.
  • A regra UNIQUE protege os dados; o try/except sqlite3.IntegrityError faz o programa reagir com educação.
  • O banco guarda o que você mandar: conferir se a saída deixa o estoque negativo é trabalho do programa.
  • Funções pequenas, cada uma com uma tarefa (abrir_banco, cadastrar, movimentar, relatorio), deixam o menu curto e fácil de ler.