Saltar al contenido

Calidad de código

Menú

Puedes leer sin crear un espacio. Créalo solo cuando quieras guardar.

Guardar progreso

Proyectos Python, dependencias y pruebas

Convierte notebooks en un paquete comprobable con límites claros y entornos reproducibles.

Lectura pública
Detalles
Reto observable

Empaqueta una transformación con dependencias fijadas y demuestra su contrato mediante pruebas unitarias, de esquema e integración.

Al terminar podrás
  • Diseñar un paquete Python modular
  • Gestionar wheels y dependencias
  • Probar DataFrames y contratos
Prerrequisitos
m12
Última revisión
25 ago 2026
Nivel
Professional
Ruta relacionada
delivery
Dominios blueprint
Developing Code · Testing
Estado
Revisión editorial interna
Fuentes principales
Python unit testing in the workspace · What are workspace files?
Reportar un error
01
Modelo mental

Estructura src y separación de I/O

Separa lógica de negocio, adaptadores Databricks y puntos de entrada para que el mismo código pueda probarse sin ejecutar un notebook completo.

Un proyecto mantenible sitúa el paquete importable bajo `src/`, las pruebas bajo `tests/` y deja los notebooks como orquestadores finos. La lógica que transforma DataFrames vive en funciones con entradas y salidas explícitas; acceso a widgets, secretos, paths y escritura se encapsula en adaptadores. Así un cambio de notebook no es la única unidad desplegable ni verificable.

`pyproject.toml` declara el paquete, versión de Python, dependencias y herramientas. El layout `src/` evita importar accidentalmente el código desde el directorio de trabajo en vez del artefacto instalado. En Databricks Runtime 16.0 o superior, un notebook no debe usarse como módulo Python: refactoriza código compartido a archivos `.py` o a una wheel.

PythonTransformación importable sin estado global
# src/orders/transforms.py
from pyspark.sql import DataFrame, functions as F

def valid_orders(df: DataFrame) -> DataFrame:
    return (
        df.where(F.col("order_id").isNotNull())
          .where(F.col("net_amount") >= 0)
          .withColumn("order_date", F.to_date("ordered_at"))
          .select("order_id", "customer_id", "order_date", "net_amount")
    )

La función no conoce catálogo, widgets ni modo de escritura; esas decisiones pertenecen al entry point del job.

¿Por qué una función de transformación no debería leer directamente `dbutils.widgets`?

Profundiza

Un notebook es una interfaz de trabajo, no una frontera arquitectónica. La lógica de negocio debería vivir en funciones y módulos puros que reciben DataFrames o valores y devuelven resultados; los adaptadores se ocupan de spark.table, widgets, secrets, escritura y APIs; el punto de entrada conecta ambos. Esta separación crea seams donde los tests sustituyen catálogos y servicios sin simular un workspace entero. También hace explícitas las dependencias y evita estado oculto de celdas ejecutadas fuera de orden. Un proyecto autosuficiente conserva notebooks delgados para exploración u orquestación, pero el comportamiento crítico se importa desde un paquete versionado que puede ejecutarse localmente, en CI y en Jobs con el mismo código.

Función pura de transformación

Función cuya salida depende sólo de entradas explícitas y no realiza lecturas, escrituras ni acceso oculto a configuración externa.

Puede probarse con datos pequeños y reutilizarse en notebook, job o pipeline sin preparar todo el entorno.
Adaptador

Componente que traduce entre la lógica interna y una dependencia concreta como Unity Catalog, REST, secrets o almacenamiento.

Aísla cambios de plataforma y permite sustituir la dependencia en tests unitarios sin falsear reglas de negocio.
Entrypoint

Punto de ejecución que carga configuración, crea dependencias y coordina lectura, transformación, validación y publicación del workload.

Mantiene orchestration visible y evita que módulos importados ejecuten efectos secundarios de forma accidental.
Resumen

Puntos clave

  • Notebooks coordinan; módulos Python implementan lógica reutilizable.
  • Separa transformaciones puras de lectura, escritura, widgets y secretos.
  • Usa layout `src/` para probar el paquete que realmente distribuyes.

Evita

  • Encapsular toda la pipeline en un notebook con variables globales y orden de celdas implícito.
  • Importar un notebook como módulo cuando el runtime moderno exige archivos Python para código compartido.
02
Implementación

Wheels, PyPI y versiones fijadas

Diseña contratos de DataFrame explícitos para que los tests detecten cambios de esquema, nulos, duplicados y semántica, no sólo errores de ejecución.

Spark evalúa de forma perezosa, por lo que construir un DataFrame no demuestra que la transformación sea válida. Las pruebas deben materializar un resultado pequeño y comparar esquema, filas y casos borde. Crea fixtures mínimas que incluyan nulos, duplicados, timestamps y valores inválidos; evita copiar datasets productivos con información sensible.

Una transformación testable recibe DataFrames y parámetros ordinarios. Para comparar resultados, ordena por claves deterministas o utiliza utilidades de aserción de DataFrames disponibles en tu stack; nunca dependas del orden natural de particiones. Separa el contrato técnico —tipos y columnas— del contrato de negocio —por ejemplo, un único pedido por ID—.

PythonPrueba unitaria de una transformación Spark
# tests/test_transforms.py
from orders.transforms import valid_orders

def test_valid_orders_rejects_negative_amounts(spark):
    source = spark.createDataFrame(
        [(1, 7, "2026-07-20T10:00:00Z", 12.5),
         (2, 8, "2026-07-20T11:00:00Z", -4.0)],
        "order_id long, customer_id long, ordered_at string, net_amount double",
    )

    actual = valid_orders(source).orderBy("order_id").collect()

    assert [row.order_id for row in actual] == [1]
    assert str(actual[0].order_date) == "2026-07-20"

El fixture prueba un comportamiento concreto; añade tests separados para nulos, zona horaria y esquema.

¿Por qué `result = transform(df)` no basta como test?

Profundiza

Un contrato de DataFrame describe significado además de columnas. Incluye nombres, tipos, nulabilidad, claves, unicidad, rangos, relaciones y reglas temporales; también aclara qué evolución es compatible. Un schema test detecta que amount cambió de decimal a string, pero no que se duplicó cada order_id o se invirtió el signo de devoluciones. Los tests de transformación deben cubrir ejemplos pequeños que representen equivalencias, nulos, duplicados, datos tardíos y límites. Comparar DataFrames exige normalizar orden o usar comparación insensible cuando el orden no forma parte del contrato. El objetivo no es replicar Spark con mocks, sino ejecutar Spark sobre casos precisos y separar lógica de invariantes de calidad de datos productivos.

Contrato semántico

Especificación de schema, claves, calidad y significado que una transformación promete aceptar y producir para sus consumidores.

Detecta regresiones que compilan y ejecutan correctamente pero alteran significado, unicidad o integridad del producto de datos.
Partición de equivalencia

Clase de entradas que deberían activar el mismo comportamiento, representada por pocos casos cuidadosamente elegidos en las pruebas.

Aporta cobertura significativa sin enumerar combinaciones infinitas ni depender de enormes copias de producción.
Comparación canónica

Normalización de orden, tipos y representación antes de contrastar dos DataFrames que deben ser semánticamente equivalentes.

Evita fallos por orden distribuido no garantizado y mantiene visibles las diferencias que sí pertenecen al contrato.
Resumen

Puntos clave

  • Materializa acciones pequeñas para ejecutar el plan bajo prueba.
  • Compara esquema y contenido con orden determinista.
  • Incluye casos borde sintéticos y libres de datos personales.

Evita

  • Afirmar sólo que `df.count()` no lanza error sin comprobar valores o esquema.
  • Comparar listas no ordenadas y aceptar tests intermitentes por particionado.
03
Operación

Funciones transform puras

Distribuye wheels reproducibles y fija dependencias en el nivel correcto para evitar que notebook, job y serverless resuelvan entornos distintos.

Una wheel empaqueta módulos y metadatos en un artefacto versionado. Declara dependencias directas en `pyproject.toml`, crea un lock o constraints para CI y fija versiones cuando la reproducibilidad lo exija. En jobs, instala la wheel como librería de la tarea; para compute clásico puedes almacenarla en workspace files o Unity Catalog Volumes según modo de acceso y runtime.

Serverless usa entornos y no admite init scripts. Fija paquetes y prueba el environment version seleccionado. Evita `%pip install` disperso por notebooks productivos: puede reiniciar Python, cambiar precedencia y hacer que dos tareas ejecuten código distinto. La promoción debe mover el mismo hash de wheel, no reconstruirla de fuentes diferentes en cada ambiente.

PythonMetadatos mínimos de paquete en pyproject.toml
# pyproject.toml
[build-system]
requires = ["hatchling==1.27.0"]
build-backend = "hatchling.build"

[project]
name = "orders-pipeline"
version = "1.4.0"
requires-python = ">=3.11"
dependencies = [
  "pydantic==2.11.7",
]

[tool.pytest.ini_options]
testpaths = ["tests"]

No declares PySpark a ciegas como dependencia runtime si lo proporciona el entorno Databricks; documenta cómo lo aporta cada target de pruebas.

¿Qué riesgo evita promover exactamente la misma wheel de test a prod?

Profundiza

Una wheel es una unidad inmutable de distribución Python: contiene código y metadatos de versión, no la promesa de que cualquier entorno resolverá dependencias igual. La reproducibilidad requiere separar dependencias de ejecución, desarrollo y plataforma, fijar rangos o lock donde corresponde y construir una vez para promover el mismo artefacto. Instalar desde una celda hace que cada run resuelva el mundo de nuevo y puede producir resultados distintos entre notebook, job y serverless. Las librerías ya proporcionadas por Databricks, como PySpark, suelen declararse de forma que el desarrollo conozca su API sin empaquetarlas innecesariamente. Las dependencias nativas exigen compatibilidad con arquitectura y runtime, no sólo un nombre en pyproject.

Wheel

Formato de distribución Python construido e instalable que empaqueta código, metadatos y puntos de entrada con una versión definida.

Crea un artefacto promovible y trazable en lugar de copiar código o reinstalarlo de forma ad hoc.
Lock de dependencias

Resolución concreta y versionada de paquetes transitivos usada para reconstruir un entorno equivalente de manera deliberada.

Reduce drift entre CI y ejecución, aunque debe actualizarse con pruebas para recibir correcciones de seguridad.
Build once, promote

Práctica de construir un artefacto una vez y mover exactamente sus mismos bytes por test y producción.

Elimina diferencias introducidas por reconstrucciones y permite atribuir el comportamiento a una versión verificable.
Resumen

Puntos clave

  • Promueve el mismo artefacto inmutable entre dev, test y prod.
  • Declara dependencias directas y controla resolución transitiva.
  • Adapta la instalación a serverless, access mode y ubicación gobernada.

Evita

  • Reconstruir la wheel en prod y obtener dependencias transitivas distintas a test.
  • Usar init scripts para gestionar paquetes de serverless, donde no están soportados.
04
Diagnóstico

assertDataFrameEqual y assertSchemaEqual

Combina tests unitarios rápidos con integración Databricks para cubrir catálogos, permisos, formatos y comportamiento distribuido.

Los tests unitarios verifican funciones y contratos con datos pequeños; no demuestran que el service principal pueda leer Unity Catalog, que la wheel se instale o que un `MERGE` sea idempotente. Las pruebas de integración despliegan en un catálogo aislado, ejecutan el entry point con identidad real y validan side effects. Deben usar nombres únicos por ejecución y limpiar sólo recursos etiquetados como temporales.

Databricks permite ejecutar pytest en el workspace y Databricks Connect puede acercar desarrollo local al compute remoto. Mantén la pirámide: muchas pruebas sin red, menos integraciones y pocas pruebas end-to-end. Marca suites y establece timeouts para que CI no ejecute por accidente una carga completa.

PythonTest de integración idempotente
import pytest

@pytest.mark.integration
def test_merge_is_idempotent(spark, target_table, sample_orders):
    sample_orders.createOrReplaceTempView("incoming_orders")
    run_merge(spark, "incoming_orders", target_table)
    first = spark.table(target_table).count()

    run_merge(spark, "incoming_orders", target_table)
    second = spark.table(target_table).count()

    assert second == first

El fixture `target_table` debe crear un nombre aislado y eliminar sólo ese objeto al finalizar, incluso si el test falla.

¿Qué fallo sólo detectaría probablemente una prueba de integración?

Profundiza

Los tests unitarios y de integración responden preguntas distintas. Un unit test pregunta si una regla produce el resultado correcto con entradas controladas y sin depender de recursos externos. Una integración pregunta si el artefacto funciona con Spark, Unity Catalog, Delta, identidades, permisos, red y configuración reales. Simular todo en unit tests no demuestra que un GRANT exista; ejecutar todos los casos en un workspace hace feedback lento y caro. La pirámide adecuada concentra combinaciones y límites en tests rápidos, y reserva pocos recorridos representativos para fronteras de plataforma. Cada test posee datos y recursos aislados, limpia lo que crea y emite evidencia suficiente para distinguir fallo funcional de fallo ambiental.

Test unitario

Prueba rápida y aislada de una regla o componente con entradas controladas y dependencias externas sustituidas por contratos simples.

Permite explorar numerosos casos límite y localizar regresiones sin pagar despliegue ni variabilidad de plataforma.
Test de integración

Prueba que ejecuta el artefacto contra servicios reales relevantes para validar compatibilidad, permisos, formatos y comportamiento distribuido.

Detecta fallos que una simulación local no reproduce, especialmente en Unity Catalog, Delta, red e identidad.
Aislamiento por run

Uso de nombres, datos y recursos exclusivos para cada ejecución automatizada de la suite de integración.

Evita colisiones entre pipelines paralelos y permite limpiar con seguridad sólo los objetos creados por esa prueba.
Resumen

Puntos clave

  • Unit tests cubren lógica; integración cubre plataforma, permisos y side effects.
  • Aísla catálogos/esquemas de prueba por ejecución.
  • Marca suites lentas y limita datos, tiempo y coste.

Evita

  • Ejecutar integraciones contra tablas compartidas y generar colisiones entre ramas.
  • Sustituir toda prueba por mocks y no detectar permisos o DDL incompatibles.
05
Decisión de diseño

Unit, integration y smoke tests

Convierte calidad en una puerta de promoción: formato, tipos, unit tests, integración, seguridad y contrato del artefacto.

Una pipeline CI debe fallar pronto: lint y type check, tests unitarios, build de wheel, escaneo y validación del bundle. La integración se ejecuta con una identidad no humana y permisos mínimos en un target efímero o compartimentado. Conserva reporte de tests, hash del artefacto y commit para reconstruir la decisión de despliegue.

Los secretos pertenecen al proveedor de identidad o al gestor de secretos del CI; prefiere OAuth workload identity/service principal a PAT de usuario. La promoción a producción requiere que el mismo artefacto superado sea referenciado por la configuración prod. Añade rollback a la versión anterior y una comprobación post-deploy antes de dirigir triggers reales.

YAMLOrden de puertas en CI
quality_gates:
  - name: unit
    command: pytest -m "not integration"
  - name: build
    command: python -m build
  - name: bundle_validate
    command: databricks bundle validate -t test
  - name: integration
    command: pytest -m integration
  - name: artifact_manifest
    command: sha256sum dist/*.whl

Adapta el sintaxis al CI elegido; el principio importante es que prod use el hash que superó estas puertas.

¿Qué dos identificadores permiten demostrar qué código llegó a producción?

Profundiza

Una puerta de promoción es una política de riesgo automatizada. No intenta demostrar que el software es perfecto; exige evidencia proporcional antes de permitir que el mismo artefacto avance. El orden suele ir de barato a costoso: formato y lint, tipos y seguridad, unit tests, build, contract tests e integración. Fallar pronto ahorra capacidad y ofrece feedback concreto. La puerta también valida el artefacto y la configuración que se desplegarán, no sólo el branch. Cobertura porcentual no sustituye casos relevantes y un scan sin política de severidad genera ruido. Las excepciones son decisiones explícitas, con responsable y caducidad, nunca un clic sin registro para saltar una señal incómoda. En automatización Databricks, el nombre vigente es Declarative Automation Bundles; Asset Bundles es el alias que todavía aparece en el blueprint Professional de 2025.

Gate de promoción

Condición automatizada y auditable que exige evidencia definida antes de mover un artefacto al siguiente ambiente de entrega.

Convierte estándares en comportamiento consistente y evita que presión temporal elimine silenciosamente pruebas críticas.
Evidencia de build

Resultados, hashes, reportes y metadatos que vinculan un commit con el artefacto exacto y las verificaciones ejecutadas.

Permite demostrar qué se probó y evita promover bytes distintos de los que recibieron aprobación.
Break-glass

Ruta excepcional y controlada para omitir temporalmente una barrera bajo autorización, registro y seguimiento obligatorios.

Permite responder a emergencias sin convertir la excepción en un bypass cotidiano invisible y permanente.
Resumen

Puntos clave

  • Falla rápido antes de consumir compute remoto.
  • CI usa identidad no humana y mínimo privilegio.
  • Firma la promoción con commit y hash del artefacto.

Evita

  • Usar el PAT personal de un desarrollador y perder el pipeline cuando cambia de equipo.
  • Desplegar desde la rama local después de que CI probó otro commit.

Vista de lectura · sin ejecución

notebook-best-practices

notebook-best-practices · commit b4f55c1

notebooks/covid_eda_modular.py

Módulo 28

Contenido del módulo