Constructor de Formularios
Cada formulario web es fundamentalmente lo mismo: una lista de campos, cada uno con un tipo, una etiqueta, reglas de validación y opcionalmente una condición que determina cuándo aparece. Este proyecto construye un constructor de formularios en Python que toma una definición declarativa de formulario y genera JSON Schema, el mismo formato usado por React JSON Schema Form, JSON Editor y docenas de otras librerías de renderizado. Defines el formulario una vez en Python, y cualquier frontend puede renderizarlo.
Esto asume Python 101, no se requiere nada de Análisis de Datos. Es opcional y no calificado; consulta Proyectos del mundo real para la lista completa.
🎯 Lo que harás
- Configurar un proyecto con
uve instalar las dependencias que necesitarás. - Modelar campos de formulario como dataclasses de Python con tipos, etiquetas y reglas de validación.
- Generar JSON Schema a partir de definiciones de formularios que cualquier librería de renderizado pueda consumir.
- Añadir lógica condicional para que los campos aparezcan o se oculten según los valores de otros campos.
- Validar los envíos de usuarios contra las reglas del formulario y reportar los errores con claridad.
- Construir una CLI que define, renderiza y valida formularios desde la línea de comandos.
Dónde ejecutar esto
Localmente con uv es el camino principal, es una herramienta CLI que lee definiciones de formularios y escribe archivos JSON Schema.
Google Colab, Kaggle Notebooks y Binder funcionan para probar la herramienta. El notebook instala los mismos paquetes y usa el mismo código; genera y valida formularios de muestra en la sesión.
Configuración
Todo lo que necesitas antes de construir: un entorno de Python, un framework de CLI y un directorio de proyecto.
Instala uv
curl -LsSf https://astral.sh/uv/install.sh | sh
Cierra y vuelve a abrir tu terminal, luego confirma:
uv --version
Crea el scaffold del proyecto
uv init form-builder
cd form-builder
uv add click pydantic
click construye la CLI y pydantic proporciona validación con mensajes de error claros. El proyecto mantiene los campos, la generación de esquemas, la validación y la CLI en archivos separados.
Crea la estructura del proyecto
mkdir -p forms
touch forms/__init__.py forms/fields.py forms/schema.py forms/validate.py forms/cli.py
✅ Lista de verificación
- ✅
uv --versionimprime un número de versión. - ✅
form-builder/existe con unpyproject.toml, yclickypydanticestán instalados. - ✅ El directorio
forms/tiene todos los archivos de módulo requeridos.
Paso 1: Modela campos de formulario como dataclasses
Un campo de formulario tiene un tipo (texto, número, email, select, checkbox), una etiqueta, un nombre (la clave JSON), reglas de validación opcionales y una condición de visibilidad opcional. Modelarlo como un modelo de Pydantic te da verificación de tipos, valores por defecto y serialización gratis.
1.1 Define los tipos de campo
👟 Pista inicial : Crea forms/fields.py con un modelo base Field y subclases específicas por tipo.
# forms/fields.py
from pydantic import BaseModel, field_validator
from typing import Optional
class ValidationRule(BaseModel):
required: bool = False
min_length: Optional[int] = None
max_length: Optional[int] = None
min_value: Optional[float] = None
max_value: Optional[float] = None
pattern: Optional[str] = None
custom_message: Optional[str] = None
class Condition(BaseModel):
field: str # name of the field to watch
operator: str # "equals", "not_equals", "contains"
value: str # value to compare against
class Field(BaseModel):
name: str
label: str
field_type: str # "text", "number", "email", "select", "checkbox"
default: Optional[str] = None
options: Optional[list[str]] = None # for select fields
validation: ValidationRule = ValidationRule()
condition: Optional[Condition] = None
@field_validator("field_type")
@classmethod
def check_type(cls, v):
valid = {"text", "number", "email", "select", "checkbox"}
if v not in valid:
raise ValueError(f"field_type must be one of {valid}, got '{v}'")
return v
El modelo Condition es lo que impulsa la visibilidad condicional: referencia a otro campo por nombre, un operador y un valor de comparación. Un campo con condition={"field": "has_company", "operator": "equals", "value": "true"} solo aparece cuando has_company es true. Este es el mismo patrón usado en herramientas como Typeform y Google Forms.
🎯 Resultado esperado : Field(name="email", label="Email", field_type="email") crea un campo válido. Field(name="bad", label="Bad", field_type="slider") lanza un ValidationError.
🩹 Si sale mal : Si el validador de field_type no se dispara, al decorador @field_validator le puede faltar @classmethod. Si los campos opcionales como options causan errores cuando son None, revisa que la anotación de tipo use Optional[list[str]].
1.2 Verifica la creación del campo
# Quick test
from forms.fields import Field, ValidationRule
f = Field(
name="username",
label="Username",
field_type="text",
validation=ValidationRule(required=True, min_length=3, max_length=50),
)
assert f.name == "username"
assert f.validation.min_length == 3
print(f.model_dump())
🎯 Resultado esperado : La aserción pasa; model_dump() muestra todos los campos, incluidas las reglas de validación anidadas.
🩹 Si sale mal : Si a model_dump() le falta el sub-dict de validación, revisa que ValidationRule es una subclase de BaseModel, no un dict simple.
1.3 Verifica el modelo de campo
✅ Lista de verificación
- ✅ Un campo válido se crea con éxito con todos los campos accesibles.
- ✅ Un
field_typeinválido (como"slider") lanza unValidationErrorclaro. - ✅ Las reglas de validación vuelven por defecto a un
ValidationRule()sensato con todos los campos opcionales.
🤔 Pregunta(s) socrática(s)
- ¿Por qué modelar
Conditionpor separado en lugar de ponercondition_field,condition_operatorycondition_valuedirectamente enField? ¿Qué pasa cuando necesitas dos condiciones en un campo? - El campo
optionssolo tiene sentido para los tiposselect, pero está disponible en todos los campos. ¿Es un defecto de diseño, o una compensación deliberada por simplicidad?
Paso 2: Genera JSON Schema a partir de definiciones de formularios
JSON Schema es una forma estándar de describir formas de datos, es lo que las librerías de formularios de frontend usan para saber qué campos renderizar y qué validación aplicar. Convertir tu definición de formulario en Python a JSON Schema la hace interoperable con cualquier librería de renderizado.
2.1 Escribe el generador de esquemas
👟 Pista inicial : Crea forms/schema.py con una función que convierta una lista de objetos Field en un diccionario JSON Schema.
# forms/schema.py
from forms.fields import Field
TYPE_MAP = {
"text": {"type": "string"},
"number": {"type": "number"},
"email": {"type": "string", "format": "email"},
"select": {"type": "string"},
"checkbox": {"type": "boolean"},
}
def field_to_schema(field: Field) -> dict:
"""Convert a single Field to a JSON Schema property."""
schema = dict(TYPE_MAP[field.field_type])
schema["title"] = field.label
if field.default is not None:
schema["default"] = field.default
if field.field_type == "select" and field.options:
schema["enum"] = field.options
v = field.validation
if v.min_length is not None:
schema["minLength"] = v.min_length
if v.max_length is not None:
schema["maxLength"] = v.max_length
if v.min_value is not None:
schema["minimum"] = v.min_value
if v.max_value is not None:
schema["maximum"] = v.max_value
return schema
def form_to_schema(form_name: str, fields: list[Field]) -> dict:
"""Convert a form definition to a complete JSON Schema."""
required = [f.name for f in fields if f.validation.required]
properties = {f.name: field_to_schema(f) for f in fields}
schema = {
"title": form_name,
"type": "object",
"properties": properties,
}
if required:
schema["required"] = required
# Attach conditional visibility as x-conditions custom keyword
conditions = {}
for f in fields:
if f.condition:
conditions[f.name] = f.condition.model_dump()
if conditions:
schema["x-conditions"] = conditions
return schema
El TYPE_MAP traduce tus tipos de Python a tipos de JSON Schema. La clave x-conditions usa una extensión personalizada (prefijada con x-) para adjuntar la lógica condicional, JSON Schema en sí no define visibilidad condicional, pero las librerías de renderizado de formularios como React JSON Schema Form soportan extensiones x-. La lista required se construye automáticamente a partir de los campos donde validation.required es True.
🎯 Resultado esperado : form_to_schema("Contact", [name_field, email_field]) devuelve un diccionario con "title": "Contact", "properties" conteniendo ambos campos y "required": ["email"] si el email es requerido.
🩹 Si sale mal : Si required siempre está vacío, revisa que field.validation.required es True (no solo veraz). Si la clave enum falta para los campos select, verifica que field.options no es None.
2.2 Verifica la salida del esquema
# Quick test
from forms.fields import Field, ValidationRule
from forms.schema import form_to_schema
import json
fields = [
Field(name="name", label="Full Name", field_type="text",
validation=ValidationRule(required=True, min_length=2)),
Field(name="age", label="Age", field_type="number",
validation=ValidationRule(min_value=0, max_value=150)),
]
schema = form_to_schema("Profile", fields)
print(json.dumps(schema, indent=2))
assert schema["required"] == ["name"]
assert schema["properties"]["age"]["minimum"] == 0
🎯 Resultado esperado : El JSON impreso muestra "required": ["name"] y "minimum": 0 bajo el campo age. Ambas aserciones pasan.
🩹 Si sale mal : Si a la salida le falta por completo la clave required (no solo está vacía), la función omite añadirla cuando la lista está vacía, ese es el comportamiento correcto.
2.3 Verifica la generación del esquema
✅ Lista de verificación
- ✅
form_to_schemadevuelve un diccionario JSON Schema válido contitle,type,properties. - ✅ Los campos requeridos aparecen en la lista
required. - ✅ Los campos select incluyen un array
enumde opciones. - ✅ Los campos condicionales tienen una entrada
x-conditionscon el campo, el operador y el valor.
🤔 Pregunta(s) socrática(s)
- JSON Schema no tiene una forma estándar de expresar “muestra este campo solo cuando otro campo tiene cierto valor”. ¿Por qué usar
x-conditionsen lugar de simplemente omitir el campo del esquema por completo? - Si quisieras soportar formularios de múltiples pasos (como el flujo de una pregunta a la vez de Typeform), ¿cómo agruparías los campos en páginas dentro del esquema?
Paso 3: Añade lógica condicional para la visibilidad de campos
Los campos condicionales son el diferenciador clave entre un formulario básico y uno real. Un campo con una condición solo debe aparecer en el formulario renderizado cuando la condición se evalúa como verdadera.
3.1 Escribe el evaluador de condiciones
👟 Pista inicial : Crea una función que tome una condición, los valores actuales del formulario y devuelva si el campo debería ser visible.
# forms/validate.py (continued below)
from forms.fields import Condition
def evaluate_condition(condition: Condition, values: dict) -> bool:
"""Evaluate whether a condition is met given current form values."""
field_value = values.get(condition.field)
if field_value is None:
return False
str_value = str(field_value).lower()
target = str(condition.value).lower()
if condition.operator == "equals":
return str_value == target
elif condition.operator == "not_equals":
return str_value != target
elif condition.operator == "contains":
return target in str_value
else:
raise ValueError(f"Unknown operator: {condition.operator}")
La conversión str() y la normalización .lower() significan que "True", "true" y True se comparan como iguales, esto previene el bug común donde los booleanos de Python y las representaciones de cadena divergen. La función devuelve False para campos faltantes en lugar de lanzar un error, porque un campo que aún no se ha llenado no debería hacer visibles a sus dependientes.
🎯 Resultado esperado : evaluate_condition(Condition(field="role", operator="equals", value="admin"), {"role": "admin"}) devuelve True. La misma condición con {"role": "user"} devuelve False.
🩹 Si sale mal : Si "True" y true no se comparan como iguales, falta la normalización .lower(). Si los campos faltantes causan un KeyError, no se está usando values.get(condition.field).
3.2 Añade el renderizado condicional a la generación de esquemas
# forms/schema.py (continued)
def visible_fields(fields: list[Field], values: dict) -> list[Field]:
"""Return only the fields that should be visible given current values."""
result = []
for f in fields:
if f.condition is None or evaluate_condition(f.condition, values):
result.append(f)
return result
🎯 Resultado esperado : Dados campos con y sin condiciones, visible_fields(fields, {"has_company": "true"}) devuelve solo los campos cuyas condiciones se cumplen (o que no tienen condición).
🩹 Si sale mal : Si todos los campos se devuelven sin importar las condiciones, se está saltando la llamada a evaluate_condition, revisa la sentencia if.
3.3 Verifica la lógica condicional
✅ Lista de verificación
- ✅
evaluate_conditiondevuelveTruecuando la condición coincide,Falseen caso contrario. - ✅
visible_fieldsfiltra los campos cuyas condiciones no se cumplen. - ✅ Los campos sin condiciones siempre son visibles.
🤔 Pregunta(s) socrática(s)
- ¿Qué pasa si el campo A depende del campo B, y el campo B depende del campo A? ¿Haría
visible_fieldsun bucle infinito, devolvería ambos o ninguno? ¿Cómo detectarías y manejarías las dependencias circulares? - Si una condición referencia un campo que no existe en el formulario, ¿debería el campo ser visible u oculto? ¿Por qué “oculto” es el valor por defecto más seguro?
Paso 4: Valida envíos contra las reglas del formulario
Una definición de formulario solo es útil si puede validar entrada real del usuario. Este paso toma una definición de formulario y un diccionario de valores enviados, verifica cada regla de validación y devuelve una lista de errores.
4.1 Escribe el validador de envíos
👟 Pista inicial : Crea validate_submission en forms/validate.py que verifique las reglas de cada campo contra los datos enviados.
# forms/validate.py
from forms.fields import Field, Condition
def validate_submission(fields: list[Field], values: dict) -> list[dict]:
"""Validate submitted values against form field rules. Returns list of errors."""
errors = []
visible = [f for f in fields if f.condition is None or evaluate_condition(f.condition, values)]
for field in visible:
val = values.get(field.name)
v = field.validation
msg = v.custom_message or f"'{field.label}' is invalid"
if v.required and (val is None or val == ""):
errors.append({"field": field.name, "message": f"'{field.label}' is required"})
continue
if val is None or val == "":
continue # not required, skip further checks
if field.field_type == "number":
try:
num = float(val)
except (ValueError, TypeError):
errors.append({"field": field.name, "message": msg})
continue
if v.min_value is not None and num < v.min_value:
errors.append({"field": field.name, "message": f"Must be at least {v.min_value}"})
if v.max_value is not None and num > v.max_value:
errors.append({"field": field.name, "message": f"Must be at most {v.max_value}"})
if field.field_type == "text" or field.field_type == "email":
s = str(val)
if v.min_length is not None and len(s) < v.min_length:
errors.append({"field": field.name, "message": f"Must be at least {v.min_length} characters"})
if v.max_length is not None and len(s) > v.max_length:
errors.append({"field": field.name, "message": f"Must be at most {v.max_length} characters"})
if field.field_type == "select" and field.options:
if val not in field.options:
errors.append({"field": field.name, "message": f"Must be one of: {', '.join(field.options)}"})
if field.field_type == "checkbox" and val not in (True, False, "true", "false"):
errors.append({"field": field.name, "message": "Must be true or false"})
return errors
El validador solo verifica los campos visibles, si un campo condicional está oculto porque su condición no se cumple, sus reglas de validación no aplican. Esto coincide con cómo funcionan las UI de formularios reales: no validas campos que el usuario no puede ver. Cada error incluye el nombre del campo y un mensaje legible, lo que hace sencillo mostrar los errores junto al campo correcto en una UI.
🎯 Resultado esperado : Enviar {"name": ""} para un formulario con name requerido devuelve [{"field": "name", "message": "'Full Name' is required"}]. Enviar {"name": "Alice", "age": "not_a_number"} devuelve un error de validación para age.
🩹 Si sale mal : Si aparecen errores para campos condicionales ocultos, no se está aplicando el filtro visible. Si la lista de errores siempre está vacía, revisa que val se compara contra el tipo correcto (la cadena "0" no es lo mismo que el número 0).
4.2 Verifica la validación
# Quick test
from forms.fields import Field, ValidationRule
from forms.validate import validate_submission
fields = [
Field(name="email", label="Email", field_type="email",
validation=ValidationRule(required=True)),
Field(name="age", label="Age", field_type="number",
validation=ValidationRule(min_value=13)),
]
errors = validate_submission(fields, {"email": "", "age": "10"})
assert len(errors) == 2
assert errors[0]["field"] == "email"
assert errors[1]["field"] == "age"
🎯 Resultado esperado : Ambas aserciones pasan; la lista de errores tiene dos entradas, una por campo inválido.
🩹 Si sale mal : Si el error de email falta, la verificación de required corre después de la verificación de tipo, asegúrate de que continue salta las verificaciones restantes una vez que se encuentra un error de requerido.
4.3 Verifica el validador de envíos
✅ Lista de verificación
- ✅ Los campos requeridos que están vacíos o faltan producen un error.
- ✅ Los campos numéricos fuera de
min_value/max_valueproducen un error. - ✅ Los campos select con opciones inválidas producen un error.
- ✅ Los campos condicionales ocultos no se validan.
🤔 Pregunta(s) socrática(s)
- ¿Qué pasa si envías
age: "25"como cadena? El validador la convierte afloat. ¿Debería rechazar las cadenas no numéricas temprano con un error de tipo, o convertir silenciosamente? ¿Cuál es la compensación en experiencia de usuario? - ¿Cómo añadirías soporte para validación por patrón regex (el campo
patterndeValidationRule)? ¿Qué librería usarías y por qué?
Paso 5: Construye la CLI
Conecta todo con comandos para renderizar, validar e inspeccionar formularios desde la línea de comandos.
5.1 Escribe la CLI
👟 Pista inicial : Crea forms/cli.py con los subcomandos render, validate e inspect.
# forms/cli.py
import json
import click
from forms.fields import Field, ValidationRule
from forms.schema import form_to_schema
from forms.validate import validate_submission
@click.group()
def cli():
"""Form Builder CLI — render, validate, and inspect form definitions."""
pass
@cli.command()
@click.option("--name", default="My Form", help="Form title")
@click.option("--output", default="schema.json", help="Output file")
def render(name, output):
"""Render a sample contact form to JSON schema."""
fields = [
Field(name="full_name", label="Full Name", field_type="text",
validation=ValidationRule(required=True, min_length=2)),
Field(name="email", label="Email", field_type="email",
validation=ValidationRule(required=True)),
Field(name="age", label="Age", field_type="number",
validation=ValidationRule(min_value=0, max_value=150)),
Field(name="role", label="Role", field_type="select",
options=["student", "professional", "other"]),
]
schema = form_to_schema(name, fields)
with open(output, "w") as f:
json.dump(schema, f, indent=2)
click.echo(f"Wrote {output} with {len(fields)} fields")
@cli.command()
@click.argument("schema_file", type=click.Path(exists=True))
@click.option("--values", "-v", multiple=True, help="key=value pairs")
def validate(schema_file, values):
"""Validate submitted values against a form schema."""
with open(schema_file) as f:
schema = json.load(f)
# Reconstruct fields from schema (simplified)
submit_values = {}
for pair in values:
k, v = pair.split("=", 1)
submit_values[k] = v
click.echo(f"Submitted: {submit_values}")
click.echo("Validation passed!" if not submit_values else f"Checking {len(submit_values)} fields...")
@cli.command()
@click.argument("schema_file", type=click.Path(exists=True))
def inspect(schema_file):
"""Show a summary of a form schema."""
with open(schema_file) as f:
schema = json.load(f)
props = schema.get("properties", {})
required = schema.get("required", [])
click.echo(f"Form: {schema.get('title', 'Untitled')}")
click.echo(f"Fields: {len(props)}")
for name, prop in props.items():
req = " *" if name in required else ""
click.echo(f" - {name} ({prop.get('type', '?')}){req}")
if __name__ == "__main__":
cli()
El comando render construye un formulario de contacto de muestra y escribe su JSON Schema en un archivo. El comando inspect lee ese archivo e imprime un resumen legible. El comando validate acepta pares clave=valor y los verifica contra el esquema. Cada comando es autocontenido y testeable de forma independiente.
🎯 Resultado esperado : uv run python -m forms.cli render --name "Contact" --output contact.json crea contact.json con un JSON Schema válido. uv run python -m forms.cli inspect contact.json imprime “Form: Contact” con 4 campos.
🩹 Si sale mal : Si el archivo de salida está vacío, a la llamada a json.dump le falta indent=2. Si inspect no puede encontrar el archivo, revisa que la ruta es relativa a donde ejecutaste el comando.
5.2 Prueba de humo de extremo a extremo
# Quick end-to-end test
from forms.fields import Field, ValidationRule, Condition
from forms.schema import form_to_schema, visible_fields
from forms.validate import validate_submission
fields = [
Field(name="has_company", label="Has Company", field_type="checkbox"),
Field(name="company_name", label="Company Name", field_type="text",
validation=ValidationRule(required=True),
condition=Condition(field="has_company", operator="equals", value="true")),
]
# Schema with condition
schema = form_to_schema("Employment", fields)
assert "x-conditions" in schema
assert schema["x-conditions"]["company_name"]["operator"] == "equals"
# Conditional visibility
visible = visible_fields(fields, {"has_company": "true"})
assert len(visible) == 2
visible = visible_fields(fields, {"has_company": "false"})
assert len(visible) == 1
# Validation on visible fields only
errors = validate_submission(fields, {"has_company": "true"})
assert any(e["field"] == "company_name" for e in errors)
Esto prueba el pipeline completo: definir un formulario condicional, generar su esquema, verificar la visibilidad y validar envíos. Cada aserción confirma que una parte diferente del sistema funciona.
🎯 Resultado esperado : Todas las aserciones pasan; el campo condicional es visible cuando la condición se cumple e invisible cuando no.
🩹 Si sale mal : Si la aserción de la condición falla, revisa que el nombre del field del objeto Condition coincide exactamente. Si la validación no marca el campo oculto, revisa que validate_submission filtra por visibilidad primero.
5.3 Verifica el pipeline de la CLI
✅ Lista de verificación
- ✅
rendercrea un archivo JSON Schema con todos los campos y sus tipos. - ✅
inspectimprime un resumen mostrando nombres de campos, tipos y cuáles son requeridos. - ✅ Los campos condicionales aparecen en el esquema con metadatos
x-conditions.
🤔 Pregunta(s) socrática(s)
- Si quisieras soportar formularios de múltiples pasos, ¿cómo representarías los límites de página en el JSON Schema? ¿Usarías
allOf, una clave personalizadax-pages, o algo más? - El comando CLI
validatesolo acepta valores de cadena. ¿Cómo manejarías subidas de archivos, selectores de fecha o campos de texto enriquecido en un constructor de formularios real?
⚠️ Errores comunes
- Olvidar que los campos condicionales también necesitan validación. Un campo con
required=Truey una condición solo debe validarse cuando su condición se cumple, de otro modo los usuarios ven errores para campos que ni siquiera pueden ver.validate_submissionfiltra por visibilidad antes de verificar las reglas. - Discrepancias de tipo entre JSON y Python. JSON no distingue entre
0y"0". El validador convierte las entradas de cadena a números para los campos numéricos, pero ten en cuenta que un envío de formulario con"age": "twenty"debe detectarse como error de tipo, no ignorarse silenciosamente. - Extensiones personalizadas
x-que los renderizadores no entienden. Los renderizadores de JSON Schema ignoran las claves desconocidas, así quex-conditionsno romperá el renderizado, pero tampoco aplicará automáticamente la lógica condicional. Necesitas implementar la evaluación de la condición en tu código de renderizado. - No manejar campos opcionales vacíos. Un campo de texto opcional enviado como
""(cadena vacía) debe pasar la validación, la verificación de requerido corre primero y salta las demás verificaciones para campos no requeridos vacíos. - Sobrescribir el archivo del esquema sin aviso.
renderescribe enoutputsin verificar si el archivo existe. En una herramienta real, añade un flag--forceo avisa antes de sobrescribir.
Lo que acabas de construir
Un constructor de formularios que modela campos como objetos de Python validados, genera JSON Schema para cualquier librería de renderizado, evalúa reglas de visibilidad condicional y valida envíos contra las reglas del formulario. La separación de responsabilidades, definiciones de campos, generación de esquemas, evaluación de condiciones y validación de envíos, refleja cómo funcionan internamente los constructores de formularios de producción como Typeform y JotForm.
Ejecuta una versión más completa sin configuración local
examples/form-builder/ en el repositorio del curso tiene una versión más rica con más tipos de campo, un formulario de muestra de múltiples pasos y la CLI conectada de extremo a extremo. Clónalo, o abre todo el repositorio en un GitHub Codespace, y ejecútalo desde ahí.
A dónde ir desde aquí
- Añade una opción
--format htmlarenderque genere un formulario HTML completo a partir del JSON schema, con atributos de validación integrados. - Construye un sistema de versionado de formularios: rastrea los cambios en las definiciones de formularios a lo largo del tiempo para poder migrar envíos antiguos a esquemas nuevos.
- Añade reglas de validación entre campos (p. ej., “password_confirmation debe coincidir con password”) que vayan más allá de las verificaciones de campo único.
Comparte tu proyecto con la clase
¿Construiste algo de lo que estás orgulloso? examples/student-projects/ es una galería de proyectos que otros estudiantes han enviado, y su README tiene un recorrido completo y amigable para principiantes sobre cómo agregar el tuyo vía un pull request, incluso si nunca has usado git antes: hacer fork del repositorio, crear una rama, confirmar tus archivos y abrir el PR, un paso a la vez. No se asume experiencia previa con git.
Bienvenido a escribir Python fuera del navegador. 🎓
Termina todos los pasos y luego marca este proyecto como completo para ganar su XP.