Técnico Universitario en Programación Guía de laboratorio · para hacer con la compu abierta

Python habla con
la base de datos

Ya sabés escribir SQL. Ya sabés programar con objetos. Lo que falta es unir las dos cosas: que tu programa Python consulte, guarde y modifique datos en MySQL — y que todo ese trabajo viva ordenado en una sola clase en vez de desparramado por todo el proyecto.

mysql-connector Conexión Cursor Parámetros CRUD Controlador Variables de entorno Mini proyecto
o usá Cmd + P (Mac) · Ctrl + P (Windows) → Guardar como PDF
Cómo se trabaja esta guía
  1. Leé la pregunta que abre cada sección y contestala mentalmente antes de seguir.
  2. Escribí el código a mano. Cada sección deja algo andando en tu computadora.
  3. Rompelo a propósito: cambiá la contraseña, borrá una coma, apagá MySQL. Mirá el error.
  4. No copies el archivo final de una. La guía lo construye de a pedazos por un motivo.
  5. Al terminar tenés un mini proyecto para entregar y seis desafíos para crecerlo.
La idea central de toda la guía

El resto del programa no tiene por qué saber
cómo se habla con la base de datos.

Lo que ya traés puesto

De Bases de Datos

Crear bases y tablas. SELECT, INSERT, UPDATE, DELETE, WHERE. Eso no cambia acá: el SQL sigue siendo el mismo SQL.

De POO

Clases, atributos, métodos, encapsulamiento. Vamos a usarlo para ordenar el acceso a datos.

De la clase 3

Instalar librerías con pip e importarlas. Hoy instalamos una más.

El recorrido
  1. 01El punto de partida: dos mundos separados
  2. 02El conector: Python + MySQL
  3. 03La primera conexión
  4. 04Ejecutar una consulta
  5. 05Consultas con parámetros
  6. 06SELECT, INSERT, UPDATE y DELETE
  7. 07El problema de repetir código
  8. 08Nuestro primer controlador
  9. 09Métodos que son acciones
  10. 10Esto también es POO
  11. 11Variables de entorno
  12. 12Mini proyecto: gestor de productos
  13. 13Desafíos por nivel
  14. 14¿Y si le ponemos PyQt?
  15. 15Hacia dónde vamos
  16. 16Ideas clave y checklist
01

El punto de partida

Dos mundos que todavía no se hablan

Sabemos escribir SQL. Sabemos programar en Python.
¿Cómo hacemos para que Python ejecute una consulta SQL sobre nuestra base?

Hasta hoy, esos dos mundos vivieron separados. El SQL lo escribiste en Workbench, en phpMyAdmin o en la consola de MySQL: escribías la consulta a mano y mirabas el resultado en pantalla. Python, mientras tanto, guardaba todo en listas y diccionarios que desaparecían al cerrar el programa.

   Python            MySQL

  listas            tablas
  objetos           filas
  variables         columnas

      ?  ←  no se conocen  →  ?
El problema: Arena de Combate olvidaba todo al cerrarse. Los datos que importan tienen que sobrevivir al Ctrl + C.
Lo que buscamos

Un puente: algo que lleve una consulta escrita en Python hasta MySQL, y traiga el resultado de vuelta convertido en datos que Python entienda.

PYTHON
▼¿con qué?
MYSQL
Para pensar antes de seguir
  • ¿Qué pasa hoy con los personajes de Arena de Combate cuando cerrás el programa?
  • Si tu programa tuviera que recordar 500 productos, ¿los guardarías en una lista?
  • ¿Qué aplicaciones que usás todos los días se acuerdan de vos? ¿Dónde estará eso guardado?
Lo importante

Esto se llama persistencia: que los datos sigan existiendo después de que el programa termina. Es la diferencia entre un ejercicio y una aplicación de verdad.

02

El conector

La librería que hace de puente

Qué es un conector

Un conector (o driver) es una librería que sabe hablar el idioma de un motor de base de datos. Traduce: recibe de Python un texto con una consulta, se lo manda a MySQL por la red, espera la respuesta y te la devuelve como listas y tuplas de Python.

Se instala igual que cualquier librería

pip install mysql-connector-python
Si usás entorno virtual, activalo antes. En algunas computadoras el comando es pip3 o python -m pip install mysql-connector-python.

Y se importa casi igual

import mysql.connector
from mysql.connector import Error
La segunda línea trae el tipo de error propio de MySQL: lo vamos a necesitar en la sección 3 para poder decir «esto falló, pero no se rompe todo».

Un detalle que confunde a todos

Lo que instalás
mysql-connector-python

Así se llama el paquete en PyPI, el repositorio de donde pip lo baja.

Lo que importás
import mysql.connector

Así se llama el módulo adentro de Python. El punto indica que connector vive dentro de mysql.

No tienen por qué coincidir, y muchas veces no coinciden. Es normal, y es una de las primeras cosas que se buscan en la documentación de cualquier librería nueva.

Comparalo con lo que ya conocés

Librería¿Se instala?¿Qué te da?
randomNo, viene con PythonNúmeros al azar. No sale de tu programa.
tkinterNo, viene con PythonVentanas. Habla con tu sistema operativo.
pygameSí, con pipUn videojuego. Habla con pantalla, teclado y sonido.
mysql.connectorSí, con pipHabla con otro programa: el servidor MySQL.
Investigá

Existen otros conectores para MySQL (PyMySQL, mysqlclient) y conectores para otros motores (psycopg2 para PostgreSQL, sqlite3 que viene incluido en Python). Buscá uno y contestá: ¿el código se parece al nuestro? Vas a descubrir que sí, y eso no es casualidad.

Antes de seguir

Necesitás MySQL corriendo en tu computadora (XAMPP, MAMP, Workbench o el servidor solo) y una base creada. Si pip install salió bien pero después nada conecta, el problema casi nunca es Python: es que el servidor está apagado.

03

La primera conexión

Cuatro datos y un apretón de manos

Todavía no vamos a crear una clase. Primero, la forma más simple que existe: un archivo, siete líneas, y ver si el servidor nos atiende.

La base con la que vamos a trabajar

CREATE DATABASE tienda;
USE tienda;

CREATE TABLE productos (
    id     INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    precio DECIMAL(10,2) NOT NULL
);

INSERT INTO productos (nombre, precio) VALUES
    ('Remera negra', 25000),
    ('Buzo gris',    48000),
    ('Gorra',        12000);
Esto ya lo sabés hacer: corrélo en Workbench o en la consola de MySQL. Es el único SQL de toda la guía que no vamos a ejecutar desde Python.

conexion.py

import mysql.connector

conexion = mysql.connector.connect(
    host="localhost",
    port=3306,
    user="root",
    password="",
    database="tienda"
)

print(conexion)
conexion.close()
Si todo salió bien vas a ver algo como <mysql.connector.connection_cext.CMySQLConnection object at 0x...>. Feo, pero significa «estás adentro».

Qué significa cada dato

hostEn qué computadora está el servidor. localhost = esta misma. Mañana puede ser una IP o un dominio.
portPor qué puerta entra. MySQL usa 3306 por defecto; si no lo ponés, se asume ese.
userQuién sos para la base de datos. En clase casi siempre root; en un servidor real, nunca.
passwordTu contraseña de MySQL. En XAMPP suele estar vacía; en Mac o Workbench la pusiste vos al instalar.
databaseCuál de todas las bases del servidor. Un mismo servidor guarda muchas.
Probá
  • Ejecutalo tal cual. ¿Imprime el objeto?
  • Cambiá database por una base que no existe. Leé el error completo.
  • Poné mal la contraseña. ¿El mensaje es distinto al anterior?
  • Cambiá el puerto a 3307. ¿Qué dice ahora?

¿Y qué pasa si la conexión falla?

Con el código de arriba, el programa explota: aparece un Traceback rojo y todo se corta ahí. Para un ejercicio da igual; para una aplicación que alguien va a usar, no. Queremos enterarnos del problema sin morir en el intento.

import mysql.connector
from mysql.connector import Error

try:
    conexion = mysql.connector.connect(
        host="localhost",
        user="root",
        password="",
        database="tienda"
    )
    print("Conexión exitosa")
    conexion.close()

except Error as e:
    print(f"Error de conexión: {e}")
try = «intentá esto». except Error as e = «si MySQL se queja, guardá la queja en e y seguí». El programa no se cae: avisa.
Error clásico

Access denied for user 'root'@'localhost' → usuario o contraseña mal.
Can't connect to MySQL server → el servidor está apagado o el puerto no es ese.
Unknown database 'tienda' → conectaste bien, pero esa base no existe todavía.
Leer el error es la mitad del trabajo: cada mensaje te dice exactamente dónde mirar.

Probá

Volvé a romper la contraseña, pero ahora con try. ¿Ves la diferencia? Antes se cortaba el programa; ahora imprime un mensaje y sigue vivo.

04

Ejecutar una consulta

Conectarse no alcanza: hay que preguntar algo

Estar conectado es como tener la llamada establecida: todavía nadie dijo nada. Para hablar necesitamos un cursor.

Cursor

El cursor es el objeto que lleva y trae. Vos le pasás la consulta, él la manda a MySQL, y después se queda con el resultado hasta que se lo pedís. Pensalo como el mozo: la conexión es estar sentado en el restaurante; el cursor es quien lleva tu pedido a la cocina y vuelve con el plato.

cursor = conexion.cursor()

cursor.execute("SELECT * FROM productos")

resultados = cursor.fetchall()

print(resultados)

cursor.close()
conexion.close()
Tres verbos nuevos y nada más: crear el cursor, ejecutar la consulta, traer el resultado.

El viaje completo

conexión
▼abre un
cursor
▼execute()
MYSQL
▼resultado
fetchall()
▼ya es Python
lista de tuplas

Qué te devuelve exactamente

[(1, 'Remera negra', Decimal('25000.00')),
 (2, 'Buzo gris',    Decimal('48000.00')),
 (3, 'Gorra',        Decimal('12000.00'))]
Una lista de tuplas. Cada tupla es una fila; cada posición, una columna en el orden del SELECT. Ya es Python puro: podés recorrerla con un for como cualquier lista.
for fila in resultados:
    print(fila[1], "-> $", fila[2])
Ese es el momento exacto en que los datos de MySQL pasan a ser datos tuyos.

fetchall() o fetchone()

fetchall()
[(1, 'Remera'), (2, 'Buzo')]

Trae todas las filas, siempre en una lista. Si no hay nada, la lista viene vacía: [].

fetchone()
(1, 'Remera')

Trae una sola fila, como tupla suelta. Si no hay nada, devuelve None. Ideal para buscar por id.

Probá
  • Cambiá el SELECT * por SELECT nombre, precio. ¿Cambia la forma de las tuplas?
  • Usá fetchone() en lugar de fetchall(). ¿Qué imprime?
  • Pedile una tabla que no existe. ¿Falla en execute() o en fetchall()?
  • Imprimí type(resultados) y len(resultados).
Para pensar
  • Si el orden de las columnas define el orden de la tupla, ¿qué pasa si mañana alguien agrega una columna en el medio de la tabla y vos usabas SELECT *?
  • ¿Conviene entonces escribir SELECT * o nombrar las columnas?
05

Consultas con parámetros

Cuando la consulta depende de lo que escribe el usuario

¿Y si quiero buscar un producto en particular, el que el usuario escriba?

La consulta ya no es fija: una parte la pone el programa y otra parte la pone alguien desde afuera. La tentación es armar el texto con un +. No lo hagas.

✗ Así no
query = "SELECT * FROM productos WHERE nombre = '" + nombre + "'"
cursor.execute(query)

Vos estás escribiendo SQL con las manos del usuario.

✓ Así sí
query = "SELECT * FROM productos WHERE nombre = %s"
cursor.execute(query, (nombre,))

Vos escribís la consulta y el conector acomoda el dato adentro.

Cómo funciona

%s es un hueco. No es «poné acá un texto»: es «acá va un dato, y lo vas a tratar como dato, pase lo que pase». La tupla del segundo argumento trae los valores que llenan esos huecos, en orden.

Por qué importa tanto

Supongamos que el usuario escribe esto en tu buscador:

Nombre a buscar:  ' OR '1'='1
Con la versión del +, la consulta que llega a MySQL termina siendo WHERE nombre = '' OR '1'='1', que es siempre verdadera: devuelve la tabla entera. Y hay textos peores que ese.
Esto tiene nombre

Se llama inyección SQL, y es una de las fallas de seguridad más viejas y más comunes que existen. Lo vas a ver a fondo en otra materia. Por ahora alcanza con la regla: ningún dato que venga de afuera se pega a la consulta con +. Nunca. Va con %s.

Además, te evita errores tontos

Si el dato es...Con +Con %s
RemeraFuncionaFunciona
Camisa D'OrsayRompe: el apóstrofo cierra la comillaFunciona
Un númeroRompe: hay que convertirlo a texto a manoFunciona
Una fechaHay que formatearla vosFunciona
La coma que todos olvidan

(nombre) no es una tupla: es simplemente nombre entre paréntesis. La tupla de un solo elemento se escribe (nombre,), con coma final. Si te olvidás, el error que aparece no menciona la coma por ningún lado — acordate de este cuadro.

Probá
  • Buscá "Gorra" con %s y fetchone().
  • Buscá algo que no exista. ¿Qué devuelve?
  • Probá dos huecos: WHERE precio > %s AND precio < %s con (10000, 30000).
  • Sacale la coma a la tupla de un elemento y mirá el error, para reconocerlo la próxima vez.
06

SELECT, INSERT, UPDATE y DELETE

El SQL de siempre, ahora disparado desde Python

La buena noticia

Las consultas ya las sabés.
Lo nuevo es solamente cómo ejecutarlas desde Python.

SELECT · leer

cursor.execute("SELECT id, nombre, precio FROM productos")
productos = cursor.fetchall()
Leer no modifica nada. Es la única de las cuatro que no necesita commit().

INSERT · crear

cursor.execute(
    "INSERT INTO productos (nombre, precio) VALUES (%s, %s)",
    (nombre, precio)
)

conexion.commit()
Fijate: no hay fetch. Un INSERT no devuelve filas. Lo que sí hay es commit().

UPDATE · modificar

cursor.execute(
    "UPDATE productos SET precio = %s WHERE id = %s",
    (precio, id_producto)
)

conexion.commit()
El orden de la tupla sigue el orden de los %s: primero el precio nuevo, después el id. Si los das vuelta, MySQL no se queja — hace otra cosa.

DELETE · borrar

cursor.execute(
    "DELETE FROM productos WHERE id = %s",
    (id_producto,)
)

conexion.commit()
La coma de la tupla de un elemento, otra vez. Y el WHERE: sin él, borrás toda la tabla.
commit()

commit() significa «confirmo estos cambios». Hasta que no lo llamás, MySQL tiene el cambio anotado en borrador pero no lo guarda de verdad. Si cerrás la conexión sin confirmar, se pierde todo lo que hiciste — y esto no da error, que es lo traicionero: tu programa dice «producto agregado» y en la tabla no hay nada.

Probá (esto hay que verlo una vez)
  • Hacé un INSERT sin commit(). Mirá la tabla en Workbench: no está.
  • Agregá el commit() y repetí. Ahora sí.
  • Después de un UPDATE, imprimí cursor.rowcount: te dice cuántas filas tocó.
  • Hacé un UPDATE con un WHERE que no matchee nada. ¿Da error o da 0?

Las cuatro, de un vistazo

Operación¿Devuelve filas?¿fetch?¿commit?
SELECTSíSíNo
INSERTNoNoSí
UPDATENoNoSí
DELETENoNoSí

Guardá mentalmente esta tabla: en la sección 8 se va a transformar, casi tal cual, en los parámetros de un método.

07

El problema de repetir código

Donde aparece la necesidad, antes que la solución

Ya sabés hacer las cuatro operaciones. Escribamos entonces el programa completo, con tres funciones nada más:

def ver_productos():
    conexion = mysql.connector.connect(host="localhost", user="root", password="", database="tienda")
    cursor = conexion.cursor()
    cursor.execute("SELECT * FROM productos")
    datos = cursor.fetchall()
    cursor.close()
    conexion.close()
    return datos

def agregar_producto(nombre, precio):
    conexion = mysql.connector.connect(host="localhost", user="root", password="", database="tienda")
    cursor = conexion.cursor()
    cursor.execute("INSERT INTO productos (nombre, precio) VALUES (%s, %s)", (nombre, precio))
    conexion.commit()
    cursor.close()
    conexion.close()

def borrar_producto(id_producto):
    conexion = mysql.connector.connect(host="localhost", user="root", password="", database="tienda")
    cursor = conexion.cursor()
    cursor.execute("DELETE FROM productos WHERE id = %s", (id_producto,))
    conexion.commit()
    cursor.close()
    conexion.close()
Funciona perfecto. Y sin embargo, mirá lo que se repite: en gris mental, todo menos una línea de cada función.

¿Y si el programa tuviera 30 consultas en vez de 3?

Se repite

30 veces connect() con los mismos cinco datos. 30 cursores. 30 close().

Se olvida

Un commit() que falta en la función 17. Un close() que nunca se llamó. Nadie se da cuenta hasta que falla.

Se rompe

Te mudás de computadora y cambia la contraseña: hay que tocar 30 lugares y no equivocarse en ninguno.

Para pensar
  • ¿Qué parte de esas tres funciones es realmente distinta entre una y otra?
  • Si lo único que cambia es la consulta y los datos... ¿por qué repetimos todo lo demás?
  • ¿Dónde viste antes esta sensación? (Pista: la clase Personaje nació igual.)

¿No sería mejor tener un solo lugar encargado de hablar con la base de datos?

La idea

Un lugar que sepa conectarse y sepa ejecutar una consulta. Todo el resto del programa le pide cosas a ese lugar y no vuelve a ver un connect() en su vida.
Ese lugar va a ser una clase, y la vamos a llamar BasededatosControlador.

Ojo con el orden

Fijate lo que acaba de pasar: primero apareció el problema, después la solución. No empezamos con una clase porque «hay que usar clases». Llegamos a la clase porque el código repetido nos empujó hasta ahí. Así nace casi todo lo que vas a aprender de ahora en adelante.

08

Nuestro primer controlador

Construido de a un pedazo, sin copiar y pegar

Vamos a armarlo en cuatro pasos. Después de cada paso, el archivo tiene que ejecutarse sin errores. Guardalo como base_de_datos.py.

Paso 1 · La clase vacía

class BasededatosControlador:
    pass
Todavía no hace nada, pero ya existe el lugar. Eso es lo importante: decidir dónde van a vivir las cosas antes de escribirlas.

Paso 2 · Que sepa conectarse

import mysql.connector
from mysql.connector import Error


class BasededatosControlador:

    def connect():
        try:
            return mysql.connector.connect(
                host="localhost",
                port=3306,
                database="tienda",
                user="root",
                password=""
            )
        except Error as e:
            print(f"Error de conexión: {e}")
            return None
Los cinco datos de la conexión quedaron escritos una sola vez en todo el proyecto. Si mañana cambia la contraseña, se toca acá y nada más.
Devuelve la conexión, o devuelve None

Este método siempre contesta algo: si pudo conectarse, te da el objeto conexión; si no pudo, avisa por pantalla y devuelve None. Quien lo llame va a poder preguntar «¿me diste una conexión o no?» antes de seguir. Eso es lo que hace el paso 3.

¿Y el self?

No hay, y no es un olvido. Hasta ahora, los métodos trabajaban sobre un objeto: goku.atacar() necesitaba saber quién es goku. Acá no hay ningún objeto que guardar: no vamos a crear «varios controladores». Hay uno solo y se usa directamente desde la clase, sin instanciar:

conexion = BasededatosControlador.connect()

En Python, la forma prolija de decir «este método no necesita objeto» es ponerle @staticmethod arriba. Funciona sin él, pero con él queda declarado. Poneselo a todos los métodos de esta clase.

@staticmethod
def connect():
    ...

Paso 3 · Que sepa ejecutar cualquier consulta

Acá está el truco de toda la guía. Volvé a mirar la tabla del final de la sección 6: las cuatro operaciones se diferencian en tres cosas nada más. Esas tres cosas van a ser los parámetros del método.

ParámetroQué preguntaPara qué sirve
query¿Qué consulta ejecuto?El SQL, con sus %s si hace falta.
params¿Con qué datos?La tupla que llena los huecos. Por defecto, ninguno.
fetchone¿Traigo una fila o todas?True = una sola. Por defecto, todas.
commit¿Esto modifica la base?True en INSERT, UPDATE y DELETE.
    @staticmethod
    def ejecutar_query(query, params=None, fetchone=False, commit=False):
        connection = BasededatosControlador.connect()

        if connection:
            try:
                with connection.cursor() as cursor:
                    cursor.execute(query, params)

                    if commit:
                        connection.commit()
                        return True

                    return cursor.fetchone() if fetchone else cursor.fetchall()

            except Error as e:
                print(f"Error en la consulta: {e}")

            finally:
                connection.close()

        return None if fetchone else []
Son 18 líneas que no vas a volver a escribir nunca más en este proyecto.

Leámoslo línea por línea

connectPide la conexión al otro método. No repite los datos: los pide donde ya están.
if«Si me dieron una conexión de verdad». Si connect() devolvió None, no se intenta nada.
withCrea el cursor y lo cierra solo al terminar el bloque, salga bien o salga mal. Es el mismo with que usás para abrir archivos.
executeLa única línea que realmente habla con MySQL. Si params es None, no pasa nada: la consulta no tenía huecos.
commitSolo confirma si se lo pidieron, y devuelve True para avisar «salió bien».
returnUna fila o todas, según lo que hayan pedido. Un if escrito en una línea.
exceptSi MySQL se queja, lo imprime. El programa no se cae: un error en una consulta no apaga la aplicación entera.
finallyCierra la conexión siempre: haya funcionado, haya fallado, haya habido un return en el medio. Esto es lo que más se olvida a mano.
últimoSi nada salió bien, devuelve algo coherente igual: None o lista vacía, según lo que esperaban. Nunca devuelve sorpresas.
Por qué importa el último return

Quien llama a ejecutar_query quiere recorrer el resultado con un for. Si ante un error devolviéramos None, ese for explotaría. Devolver [] significa «no hubo datos» y el for simplemente no da vueltas. Un buen método falla sin arruinarle el día a quien lo usó.

Paso 4 · Mirá lo que quedó

TODO EL PROGRAMA
▼le pide
BasededatosControlador
▼connect() + ejecutar_query()
MYSQL
Probá

Al final del archivo, agregá estas dos líneas y ejecutalo:

productos = BasededatosControlador.ejecutar_query("SELECT * FROM productos")
print(productos)

Una línea para pedir. Sin connect, sin cursor, sin close. Ese es todo el punto.

¿Y si...?

Apagá el servidor de MySQL y volvé a ejecutar. ¿Qué imprime? ¿Se cae el programa o avisa? ¿En cuál de los dos except entró — el de connect o el de ejecutar_query?

09

Métodos que son acciones

Del SQL suelto al vocabulario de tu aplicación

Ya podemos ejecutar cualquier consulta desde un solo lugar. Pero todavía hay algo incómodo: el SQL sigue apareciendo desparramado por el programa, cada vez que alguien quiere algo. Démosle nombre a esas cosas que el programa quiere.

Traer todo

    @staticmethod
    def traer_productos():
        query = "SELECT id, nombre, precio FROM productos"
        return BasededatosControlador.ejecutar_query(query)
Dos líneas. Toda la máquina pesada ya está escrita.

Buscar uno

    @staticmethod
    def buscar_producto(nombre):
        query = """
            SELECT id, nombre, precio
            FROM productos
            WHERE nombre = %s
        """
        return BasededatosControlador.ejecutar_query(
            query,
            (nombre,),
            fetchone=True
        )
Las triples comillas permiten escribir el SQL en varias líneas, tal como lo escribirías en Workbench. Para consultas largas se lee muchísimo mejor.

Agregar

    @staticmethod
    def agregar_producto(nombre, precio):
        query = "INSERT INTO productos (nombre, precio) VALUES (%s, %s)"
        return BasededatosControlador.ejecutar_query(
            query,
            (nombre, precio),
            commit=True
        )
Lo único que cambia respecto del anterior es commit=True en vez de fetchone=True. El patrón ya es siempre el mismo.
Lo que acaba de pasar

El resto del programa ya no necesita saber cómo se habla con MySQL. No sabe qué es un cursor, no sabe qué es commit, no conoce la contraseña. Solo pide lo que necesita, con palabras de tu aplicación:

productos = BasededatosControlador.traer_productos()
uno       = BasededatosControlador.buscar_producto("Gorra")
BasededatosControlador.agregar_producto("Campera", 90000)

Tres capas, cada una con su trabajo

CapaSabeNo sabe
traer_productos()Qué consulta hace faltaCómo se ejecuta
ejecutar_query()Cómo se ejecuta cualquier consultaQué consulta le van a pasar
connect()Dónde está la base y con qué usuario entrarQué se va a hacer adentro

Cada una hace una cosa sola y confía en la de abajo. Si mañana cambia la contraseña se toca connect(); si cambia el nombre de una columna se toca traer_productos(). Un cambio, un lugar.

Fijate en los nombres

traer_productos, buscar_producto, agregar_producto. No se llaman select1, query_a ni funcion2. Los nombres cuentan qué quiere hacer tu aplicación, no cómo lo hace por dentro. Leer la lista de métodos de esta clase debería ser como leer el índice de lo que tu programa puede hacer.

Probá
  • Escribí los tres métodos y llamalos desde el final del archivo.
  • Buscá un producto que no exista. ¿Qué devuelve buscar_producto?
  • Agregá uno y volvé a traer todos. ¿Aparece el nuevo?
  • Mirá la tabla en Workbench para confirmar que se guardó de verdad.
10

Esto también es POO

Un paso atrás para mirar lo que construimos

No lo dijimos en el camino, pero lo que acabás de hacer es exactamente lo que venís practicando desde la clase 1. Contestá estas preguntas sobre tu propia clase:

Sobre BasededatosControlador
  • ¿Qué es? ¿Un personaje, una cosa del mundo real... o algo distinto?
  • ¿Qué métodos tiene? ¿Cuál usa a cuál?
  • ¿Cuál es su responsabilidad? ¿Podrías decirla en una sola frase?
  • ¿Qué información maneja que el resto del programa no ve?
  • ¿Qué pasa si mañana la base se muda a otro servidor? ¿Cuántos archivos tocás?
Personaje

Representa una cosa: tiene vida, fuerza, nombre. Creás muchos: goku, freezer. Cada uno guarda sus datos.

BasededatosControlador

Representa una responsabilidad: hablar con la base. No hay muchos, hay uno. No guarda datos propios: guarda un saber hacer.

Las dos son clases perfectamente válidas. Una clase no sirve solamente para modelar sustantivos del mundo real: también sirve para juntar en un lugar todo lo que tiene que ver con un mismo asunto.

Encapsulamiento, otra vez

Encapsular también significa esconder detalles
que el resto del programa no necesita conocer.

Cuando aprendiste encapsulamiento, se trataba de proteger atributos: que nadie le ponga vida negativa a un personaje. Es la misma idea, un escalón más arriba. Acá lo que está escondido es todo un procedimiento: la contraseña, el cursor, el commit, el cierre. Quien usa la clase no necesita saber nada de eso, igual que vos no necesitás saber cómo funciona print() por dentro.

La palabra que estamos estrenando

A una clase así se le suele decir servicio o controlador: no representa una cosa, presta un servicio al resto del programa. Es el primer nombre nuevo de una familia que vas a seguir conociendo más adelante en la carrera. Por ahora, con la idea alcanza: un lugar, una responsabilidad.

¿Y si...?

¿Y si mañana la aplicación tuviera que guardar en un archivo de texto en vez de MySQL? Fijate que podrías cambiar todo el interior de ejecutar_query y connect, y el resto del programa seguiría llamando igual a traer_productos() sin enterarse de nada. Eso es lo que hace tan valiosa esta separación.

11

Variables de entorno

Sacar la contraseña del código

¿Qué tiene de malo esto?

password="123456"
Se publica

Subís el proyecto a GitHub para entregarlo y tu contraseña queda a la vista de todos. Y queda en el historial aunque después la borres.

No se comparte

Tu compañero tiene otra contraseña de MySQL. Cada uno tendría que editar el archivo, y alguno lo va a subir cambiado.

No se muda

En tu compu la base está en localhost. En el servidor, no. Mismo código, distintos datos.

Variable de entorno

Es un dato que no vive en tu programa sino en el ambiente donde el programa corre: tu computadora, la de tu compañero, el servidor. El código pregunta «¿cuál es la contraseña acá?» y cada lugar responde la suya.

import os

usuario  = os.getenv("DB_USER")
password = os.getenv("DB_PASSWORD")
os.getenv("DB_USER") = «traeme el valor que el ambiente guardó con ese nombre». Si no existe, devuelve None.

Con un valor por defecto

host = os.getenv("DB_HOST", "localhost")
port = int(os.getenv("DB_PORT", "3306"))
El segundo argumento es el plan B: «si nadie definió DB_HOST, usá localhost». Así el proyecto funciona recién clonado, sin configurar nada.
Y ojo con int(): las variables de entorno siempre son texto, y el puerto tiene que ser número.

El archivo .env

La forma más cómoda de definirlas es un archivo llamado .env (con punto adelante) al lado de tu programa:

DB_HOST=localhost
DB_PORT=3306
DB_NAME=tienda
DB_USER=root
DB_PASSWORD=mi_clave_secreta
Sin comillas, sin espacios alrededor del =, una variable por línea.
1 · instalarpip install python-dotenv — la librería que sabe leer ese archivo.
2 · cargarArriba de todo en tu programa: from dotenv import load_dotenv y después load_dotenv().
3 · usarA partir de ahí, os.getenv("DB_USER") encuentra lo que pusiste en el .env.
4 · ocultarAgregá una línea con .env a tu archivo .gitignore. El .env nunca se sube.
5 · avisarSubí en cambio un .env.example con los nombres y sin los valores, para que el que clone el proyecto sepa qué tiene que completar.

connect(), versión final

import os
import mysql.connector
from mysql.connector import Error
from dotenv import load_dotenv

load_dotenv()


class BasededatosControlador:

    @staticmethod
    def connect():
        try:
            return mysql.connector.connect(
                host=os.getenv('DB_HOST', 'localhost'),
                port=int(os.getenv('DB_PORT', '3306')),
                database=os.getenv('DB_NAME', 'tienda'),
                user=os.getenv('DB_USER', 'root'),
                password=os.getenv('DB_PASSWORD', '')
            )
        except Error as e:
            print(f"Error de conexión: {e}")
            return None
Este código ya se puede mostrar, subir y compartir: no dice ningún secreto. Dice dónde buscarlos.
Probá
  • Creá el .env, instalá python-dotenv y confirmá que sigue conectando.
  • Cambiá DB_NAME en el .env por una base que no existe. ¿Avisa?
  • Borrá el .env entero. ¿Sigue funcionando? ¿Por qué? (Pista: los valores por defecto.)
  • Imprimí os.getenv("DB_PASSWORD") para ver que lo está leyendo de verdad.
Dónde entra esto más adelante

Cuando tu aplicación deje tu computadora y se vaya a un servidor, nada del código va a cambiar: solo van a cambiar las variables de entorno de ese lugar. Es la razón por la que se hace así en todos lados. Cómo se sube una aplicación a un servidor es tema de otra guía.

12

Mini proyecto: gestor de productos

Una aplicación de consola que recuerda

La consigna

Una aplicación con un menú de consola que permita ver, buscar, agregar, modificar y eliminar productos. Toda la comunicación con MySQL tiene que pasar por BasededatosControlador: si en main.py aparece la palabra cursor, algo salió mal.

Sin PyQt por ahora

La interfaz es de consola, a propósito. Hoy lo que estamos aprendiendo es la comunicación Python ↔ MySQL, y una ventana de por medio solo agregaría ruido. La ventana viene en la sección 14 — y vas a ver que casi no hay que tocar nada.

Dos archivos, dos responsabilidades

base_de_datos.py

La clase BasededatosControlador. Sabe de MySQL. No sabe que existe un menú.

main.py

El menú, los input() y los print(). Sabe hablar con el usuario. No sabe nada de MySQL.

El menú

===== PRODUCTOS =====

1. Ver productos
2. Buscar producto
3. Agregar producto
4. Modificar producto
5. Eliminar producto
6. Salir

Opción: _

El esqueleto de main.py

from base_de_datos import BasededatosControlador


def mostrar_menu():
    print("\n===== PRODUCTOS =====")
    print("1. Ver productos")
    print("2. Buscar producto")
    print("3. Agregar producto")
    print("4. Modificar producto")
    print("5. Eliminar producto")
    print("6. Salir")


def ver_productos():
    productos = BasededatosControlador.traer_productos()

    if not productos:
        print("No hay productos cargados.")
        return

    for id_p, nombre, precio in productos:
        print(f"[{id_p}] {nombre} — ${precio}")


def agregar_producto():
    nombre = input("Nombre: ")
    precio = input("Precio: ")

    if BasededatosControlador.agregar_producto(nombre, precio):
        print("Producto agregado.")
    else:
        print("No se pudo agregar.")


# TODO: buscar_producto(), modificar_producto(), eliminar_producto()


while True:
    mostrar_menu()
    opcion = input("\nOpción: ")

    if opcion == "1":
        ver_productos()
    elif opcion == "3":
        agregar_producto()
    elif opcion == "6":
        print("Hasta luego.")
        break
    else:
        print("Opción inválida.")
Está incompleto a propósito: las opciones 2, 4 y 5 las completás vos con los desafíos de la sección 13. Fijate que main.py no importa mysql.connector por ningún lado.
Un detalle lindo

for id_p, nombre, precio in productos: desarma cada tupla en tres variables con nombre. Es mucho más legible que fila[0], fila[1], fila[2] — y te obliga a saber qué columnas pediste.

Lo que tiene que cumplir tu entrega
□Dos archivos: base_de_datos.py y main.py.
□Las 5 operaciones del menú funcionando.
□Ni un connect() ni un cursor fuera del controlador.
□Todas las consultas con datos del usuario usan %s.
□INSERT, UPDATE y DELETE con commit=True.
□Los datos de conexión en un .env, no en el código.
□Si la base está apagada, el programa avisa y no explota.
□Si buscás algo que no existe, el programa lo dice con palabras.

Se prueba así: cargá tres productos desde tu programa, cerralo, abrí Workbench y mirá la tabla. Volvé a abrir tu programa. Tienen que seguir ahí. Ese es el momento en que tu aplicación dejó de ser un ejercicio.

13

Desafíos por nivel

De a uno, y probando cada uno antes de seguir

Todos se resuelven agregando un método a BasededatosControlador y llamándolo desde el menú. Ninguno necesita tocar ejecutar_query.

Nivel 1 · traer_productos()

Devolver todos los productos. Ya está resuelto en la sección 9: escribilo de memoria, sin mirar, y conectalo con la opción 1 del menú.

Pista: no lleva params, no lleva fetchone, no lleva commit.

Nivel 2 · buscar_producto(nombre)

Recibe un nombre y devuelve un solo producto. Opción 2 del menú.

  • ¿Qué devuelve si no lo encuentra? Tu menú tiene que contemplarlo.
  • Mejora: hacelo con LIKE para que «reme» encuentre «Remera». Investigá cómo se arma el % del LIKE junto con el %s del parámetro — no es tan obvio como parece.
Nivel 3 · agregar_producto(nombre, precio)

Inserta un producto nuevo. Opción 3 del menú.

  • Acordate del commit=True.
  • ¿Qué pasa si el usuario escribe "caro" en el precio? Probalo y decidí qué hacer.
  • Validá antes de guardar: nombre no vacío, precio mayor que cero.
Nivel 4 · actualizar_producto(id, nombre, precio)

Modifica un producto existente. Opción 4 del menú.

  • Mostrale primero la lista al usuario para que sepa qué id elegir.
  • Cuidado con el orden de la tupla: los %s del SET van antes que el del WHERE.
  • Si el id no existe, no da error pero no cambia nada. ¿Cómo te enterás? Investigá cursor.rowcount.
Nivel 5 · eliminar_producto(id)

Borra un producto. Opción 5 del menú.

  • Pedí confirmación antes de borrar. Esto no se puede deshacer.
  • Revisá tres veces que el WHERE esté. Probalo con la base de prueba, nunca con datos que te importen.
Nivel 6 · ¿Y si hay dos tablas?

Agregá una tabla clientes (id, nombre, email) y hacé que el programa maneje las dos.

  • Escribí traer_clientes(), agregar_cliente(), etcétera.
  • La pregunta importante: ¿cuánto código nuevo tuviste que escribir para la segunda tabla? ¿Tocaste connect()? ¿Tocaste ejecutar_query()?
  • Si la respuesta fue «no toqué nada de eso», entendiste de qué se trataba la guía.

Para pensar, sin resolverlo todavía: si los métodos de productos y los de clientes empiezan a ser muchos, ¿te seguiría cerrando tenerlos en la misma clase? ¿Qué harías? Guardate la respuesta: es la pregunta con la que arranca la próxima etapa.

¿Y si...? (para los que terminaron)
  • ¿Y si querés los productos ordenados por precio? ¿Dónde va el ORDER BY: en Python o en el SQL? ¿Por qué conviene que sea en el SQL?
  • ¿Y si querés saber cuántos productos hay sin traerlos todos?
  • ¿Y si un producto tuviera un cliente_id? ¿Podrías traer el nombre del cliente junto con el producto en una sola consulta?
  • ¿Y si tu programa tuviera que hacer 500 inserts? Investigá executemany.
14

¿Y si le ponemos PyQt?

El puente hacia la aplicación completa

¿Qué pasaría si mañana le ponemos a esto la interfaz que aprendimos con PyQt?

PyQt
▼el usuario pide
lógica Python
▼traer / guardar
BasededatosControlador
▼SQL
MYSQL
   Buscar producto

Nombre: [ gorra________ ]

        [  BUSCAR  ]

        ↓

   Gorra
   $12.000
Una pantalla que ya sabrías construir con lo de la guía de PyQt.

¿Cuánto habría que cambiar del controlador?

Nada

Donde la consola hacía print(), la ventana va a hacer etiqueta.setText(). Donde había input(), va a haber campo.text(). La línea del medio es exactamente la misma:

producto = BasededatosControlador.buscar_producto(nombre)
En consola
nombre = input("Nombre: ")
p = BasededatosControlador.buscar_producto(nombre)
print(p)
En PyQt
nombre = campo.text()
p = BasededatosControlador.buscar_producto(nombre)
etiqueta.setText(str(p))
La idea que queda

La interfaz no tiene por qué saber
cómo funciona MySQL. Solo pide.

Y funciona en los dos sentidos: la base de datos tampoco sabe que existe una ventana. Por eso el mismo controlador sirve para una app de consola, para una de escritorio y para lo que venga después. Escribiste el controlador una vez; la interfaz podés cambiarla tres veces sin tocarlo.

Si querés adelantarte

Agarrá el mini proyecto de la sección 12 y reemplazá solamente main.py por una ventana PyQt con un campo, un botón y una etiqueta. No toques base_de_datos.py. Si lo lográs sin abrir ese archivo ni una vez, acabás de comprobar vos mismo de qué sirve separar responsabilidades.

15

Hacia dónde vamos

Las piezas ya están: falta armarlas juntas

INTERFAZ
PyQt / Tkinter
▼pide
LÓGICA DEL PROGRAMA
tus clases, tus reglas
▼consulta
SERVICIOS / CONTROLADORES
acceso a datos
▼SQL
MYSQL
los datos
CapaCuándo la aprendisteQué resuelve
InterfazClase 3 y guía de PyQtQue una persona pueda usar tu programa.
LógicaClases 1 y 2, Arena de CombateLas reglas: qué puede y qué no puede pasar.
ServiciosEsta guíaTraer y guardar datos sin ensuciar el resto.
DatosBases de DatosQue la información sobreviva al programa.
Dónde estás parado

Hasta ahora aprendimos las piezas por separado. Cada una funcionaba sola y se entendía sola. El próximo paso es aprender a unirlas: una sola aplicación que tenga interfaz, lógica y datos, y que no se convierta en un archivo de 800 líneas imposible de tocar.

Preguntas que todavía no contestamos (y está bien)
  • ¿Dónde vive la regla «un producto no puede tener precio negativo»: en la interfaz, en la lógica o en la base?
  • Si tuviéramos ocho tablas, ¿seguiría siendo una sola clase controladora?
  • ¿Cómo hacemos para que una tupla (1, 'Gorra', 12000) vuelva a ser un objeto Producto con atributos y métodos?
  • ¿Cómo se prueba que todo esto anda, sin abrir el programa y probar a mano cada vez?

Cada una de esas preguntas es una clase futura. Tenerlas en la cabeza antes de que te las contesten es exactamente lo que hace que después se entiendan.

16

Ideas clave

Lo que te tenés que llevar

    ANTES

   Python
Todo en memoria. Al cerrar, se perdía.
    AHORA

   Python
     ↕
   MySQL
Los datos sobreviven al programa.
   DESPUÉS

  Interfaz
     ↓
   Python
     ↓
  Servicios
     ↓
   MySQL
Cada capa con su responsabilidad.
Las siete ideas de esta guía
  1. Una librería amplía lo que tu programa puede hacer; un conector le permite hablar con otro programa: el servidor de base de datos.
  2. El SQL sigue siendo SQL. Lo que aprendiste en Bases de Datos no cambia: Python se encarga de ejecutarlo y de traer el resultado.
  3. Los datos que vienen de afuera van con %s, nunca pegados con +.
  4. Leer no necesita commit(). Modificar, sí — y si falta, no hay error: simplemente no se guardó nada.
  5. Una clase puede centralizar una responsabilidad, aunque no represente una cosa del mundo real.
  6. Los métodos pueden tener el nombre de las acciones de tu aplicación, no el de las consultas que las resuelven.
  7. La interfaz no necesita conocer la base de datos. Separar responsabilidades es lo que permite que un proyecto crezca sin volverse inmanejable.

Checklist final

□Sé instalar e importar el conector de MySQL.
□Sé conectarme a una base desde Python y explicar los cinco datos.
□Sé qué es un cursor y para qué sirve.
□Sé ejecutar un SELECT y recorrer el resultado.
□Sé la diferencia entre fetchone() y fetchall().
□Sé usar parámetros con %s y puedo explicar por qué.
□Sé hacer INSERT, UPDATE y DELETE, y no me olvido del commit().
□Puedo explicar qué problema resuelve BasededatosControlador.
□Entiendo qué hacen try, except y finally ahí adentro.
□Puedo agregar un método nuevo al controlador sin tocar lo anterior.
□Sé sacar la contraseña del código con variables de entorno.
□Mi mini proyecto anda, y los datos siguen ahí al volver a abrirlo.
□Puedo explicarle a alguien por qué la interfaz no debería conocer la base.
Una última cosa

Si algún casillero quedó vacío, no sigas para adelante: volvé a esa sección. Todo lo que viene después se apoya en esto, y se apoya fuerte.

Ya no hacemos solamente programas que funcionan.
Empezamos a construir programas que pueden crecer.

Un programa que anda es el primer objetivo. Un programa que se puede modificar seis meses después, sin miedo y sin romper nada, es el oficio.

Guía de laboratorio · Python + MySQL · Técnico Universitario en Programación Continúa Clase 3, la guía de PyQt y Arena de Combate