Esta lección cierra el módulo 2 atando dos cabos que llevamos arrastrando desde hace tiempo.

El primero se descubrió con lápiz y papel, en la prueba de escritorio de la lección Del problema al algoritmo: cuando Marta tecleaba ALTA en mayúsculas, el algoritmo rechazaba una prioridad perfectamente correcta, porque para un ordenador "ALTA" y "alta" son dos textos distintos. Prometimos resolverlo aquí.

El segundo apareció en la lección anterior: input() devuelve siempre un str, así que en cuanto TareaFácil quiera pedir los días estimados o las horas dedicadas y hacer cuentas con ellos, se estrellará con un TypeError. Por eso esos datos siguen escritos a mano en el código.

Ambos son manifestaciones del mismo principio, que es la idea de fondo de esta lección: todo dato que viene de fuera del programa es sospechoso hasta que se demuestre lo contrario. Puede tener el tipo equivocado, sobrarle espacios, venir en mayúsculas, estar vacío o ser directamente un disparate. El trabajo de convertirlo al tipo correcto se llama conversión; el de comprobar que tiene sentido, validación. Un programa que no hace las dos cosas no es un programa terminado: es un prototipo que funciona mientras nadie lo use en serio.

Aprenderás a convertir con int(), float(), str() y bool(), a interpretar los errores ValueError y TypeError, a sanear texto con .strip(), .lower() y .capitalize(), y a comprobar datos antes de usarlos con .isdigit(), .isalpha() y el operador in.

Contenido

  1. Los dos cabos sueltos, en código
  2. Conversión explícita: int(), float(), str()
  3. Qué conversiones funcionan y cuáles fallan: el ValueError
  4. Conversión implícita, y por qué texto y número no se mezclan
  5. bool() y las reglas de veracidad
  6. Normalizar el texto: .strip(), .lower() y .capitalize()
  7. Comprobar antes de convertir: .isdigit() e .isalpha()
  8. Validar contra una lista de valores permitidos
  9. Qué es validar y por qué se valida siempre
  10. Un if mínimo, y lo que aún nos falta
  11. TareaFácil: sanear y validar la entrada
  12. Errores comunes y consejos
  13. Ejercicios
  14. Conclusión

  1. Los dos cabos sueltos, en código

Empecemos viendo los dos problemas con toda su crudeza.

Cabo suelto 1: lo que llega de input() es texto.

dias = input("Dias estimados: ")     # el usuario teclea 3

print(type(dias))        # <class 'str'>
print(dias + 1)          # TypeError
TypeError: can only concatenate str (not "int") to str

Cabo suelto 2: el texto no se compara como lo escribe una persona.

prioridad = input("Prioridad: ")     # el usuario teclea ALTA

print(prioridad == "alta")           # False

Y hay más variantes del mismo problema, todas verosímiles:

Lo que teclea el usuario prioridad == "alta" Por qué
alta True Coincide exactamente
ALTA False Mayúsculas distintas
Alta False La primera letra difiere
alta (con espacio final) False El espacio es un carácter más
alta (espacio delante) False Ídem
urgente False No es una prioridad válida
(Intro sin escribir nada) False Es la cadena vacía ""

De las siete filas, solo la primera es lo que queríamos, la última es realmente un error del usuario, y las cinco intermedias son entradas razonables que un programa decente debería aceptar o rechazar con criterio, no por accidente.

Las cinco primeras se arreglan convirtiendo y normalizando; las dos últimas, validando. Vamos con ello.

  1. Conversión explícita: int(), float(), str()

Python trae funciones que transforman un valor de un tipo a otro. Se llaman igual que el tipo destino y se usan poniendo el valor entre paréntesis.

Función Convierte a Ejemplo Resultado
int(x) Entero int("3") 3
float(x) Decimal float("3.5") 3.5
str(x) Texto str(3.5) "3.5"
bool(x) Booleano bool("") False

A esto se le llama conversión explícita o casting: eres tú quien la pide, en un punto concreto del código.

int(): de texto a entero

Resuelve el primer cabo suelto de golpe:

dias_texto = input("Dias estimados: ")     # el usuario teclea 3

dias = int(dias_texto)

print(type(dias))            # <class 'int'>
print(dias + 1)              # 4    <- ahora si es aritmetica
print(dias * 2)              # 6    <- ya no repite el texto

Es habitual escribirlo todo en una línea, envolviendo el input() directamente:

dias = int(input("Dias estimados: "))

Se lee de dentro afuera: primero se ejecuta input(), que devuelve texto; ese texto se le pasa a int(), que devuelve un entero; y el entero se asigna a dias. Es compacto y muy común, pero tiene un inconveniente que veremos en el apartado 7: si el usuario teclea algo que no es un número, el programa se detiene ahí mismo.

int() también convierte decimales a enteros, y aquí hay que fijarse en un detalle:

print(int(3.9))      # 3    <- NO redondea a 4
print(int(3.1))      # 3
print(int(-3.9))     # -3   <- corta hacia cero

int() trunca, no redondea: elimina la parte decimal sin mirarla. Si quieres redondear al entero más cercano, usa round():

print(round(3.9))    # 4
print(round(3.1))    # 3

Confundir int() con round() produce errores silenciosos en cálculos de importes y porcentajes. Anótalo.

float(): a decimal

horas = float(input("Horas dedicadas: "))     # el usuario teclea 7.5

print(horas)         # 7.5
print(type(horas))   # <class 'float'>

float() acepta también texto sin decimales, y lo convierte añadiendo .0:

print(float("3"))    # 3.0

Pero no acepta la coma decimal española:

print(float("3,5"))
ValueError: could not convert string to float: '3,5'

Es un problema real con usuarios españoles, que teclean la coma con toda naturalidad. Un programa cuidadoso reemplaza la coma por un punto antes de convertir. Eso se hace con el método .replace(), que pertenece a Cadenas de caracteres; lo menciono para que sepas que existe solución.

str(): a texto

Es la conversión inversa: convierte cualquier valor en su representación textual.

dias = 7
completada = False
fecha = None

print(str(dias))          # 7      (pero ahora es un str)
print(str(completada))    # False
print(str(fecha))         # None

print(type(str(dias)))    # <class 'str'>

Su uso clásico es concatenar un número con texto usando +, lo que sin conversión daba TypeError:

dias = 7

print("Quedan " + dias + " dias")          # TypeError
print("Quedan " + str(dias) + " dias")     # Quedan 7 dias

Dicho esto: en código nuevo casi nunca necesitarás str() para esto, porque las f-strings de la lección anterior hacen la conversión solas y quedan mucho mejor:

print(f"Quedan {dias} dias")               # Quedan 7 dias

str() sigue siendo útil en otros contextos —lo usamos en la ficha de TareaFácil para alinear un booleano como si fuera texto—, pero no como forma habitual de componer mensajes.

  1. Qué conversiones funcionan y cuáles fallan: el ValueError

Convertir no siempre es posible. Esta tabla resume lo que puedes esperar:

Conversión Resultado ¿Funciona?
int("3") 3
int(" 3 ") 3 Sí — int() ignora los espacios de los extremos
int("3.0") NoValueError
int("tres") NoValueError
int("") NoValueError
int(3.9) 3 Sí (trunca)
int(True) 1 Sí (True es 1, False es 0)
float("3.5") 3.5
float("3") 3.0
float("3,5") NoValueError
float("abc") NoValueError
str(cualquier cosa) Su texto Siempre funciona
bool(cualquier cosa) True o False Siempre funciona

Hay tres filas que merecen comentario:

  • int(" 3 ") funciona. int() es tolerante con los espacios de los extremos. No lo es con nada más.
  • int("3.0") falla. Sorprende, porque 3.0 es un número. Pero para int(), el texto "3.0" contiene un punto que no pertenece a un entero. Si esperas que el usuario pueda escribir decimales, convierte con float() y trunca después: int(float("3.0")) da 3.
  • str() y bool() nunca fallan. Cualquier valor tiene representación textual y cualquier valor es verdadero o falso. Eso los hace seguros, pero también silenciosos: no te avisarán de un dato absurdo.

Leer un ValueError

Cuando la conversión no puede hacerse, Python lanza un ValueError: el tipo era el correcto (le has pasado un texto a int(), que es lo que espera), pero el valor concreto no sirve.

dias = int("tres")
Traceback (most recent call last):
  File "tareafacil.py", line 4, in <module>
    dias = int("tres")
           ^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'tres'

Un mensaje de error de Python se lee de abajo arriba, y contiene más información de la que parece:

Parte Qué te dice
ValueError El tipo de error: el valor no es convertible
invalid literal for int() Estabas intentando convertir a entero
with base 10 En base decimal (la normal)
'tres' El valor exacto que ha fallado
File "tareafacil.py", line 4 El fichero y la línea donde ha ocurrido

Esa última fila es la más valiosa y la que más gente ignora: el error te dice dónde está el problema. No leas solo la última línea; mira también el número de línea.

Y no confundas los dos errores que aparecen al convertir:

Error Significa Ejemplo
ValueError El valor no sirve para esa conversión int("tres")
TypeError El tipo no admite esa operación "3" + 4

Cuando un ValueError ocurre, el programa se detiene. Todo lo que hubiera después no se ejecuta. Hay dos maneras de evitarlo: comprobar antes de convertir (lo que haremos en el apartado 7) o capturar el error cuando ocurre, con try / except, que se estudia en Depuración y manejo de errores.

  1. Conversión implícita, y por qué texto y número no se mezclan

Hay conversiones que Python hace solo, sin que se las pidas. Se llaman implícitas, y ocurren únicamente entre tipos numéricos.

resultado = 3 + 1.5

print(resultado)         # 4.5
print(type(resultado))   # <class 'float'>

Python ha convertido el 3 (un int) a 3.0 (un float) para poder sumarlo con 1.5. La regla es sencilla: cuando se mezclan int y float, el resultado es siempre float, porque es el tipo más «amplio», el que puede representar los valores del otro sin perder información.

print(type(5 + 2))       # <class 'int'>
print(type(5 + 2.0))     # <class 'float'>
print(type(5 / 2))       # <class 'float'>   <- recuerda: / siempre da float
print(type(5 // 2))      # <class 'int'>

Los booleanos también participan, porque internamente True vale 1 y False vale 0:

print(True + True)       # 2
print(False + 10)        # 10

Es una curiosidad más que una técnica; no escribas código que dependa de ello.

Por qué Python no convierte texto y número

Y ahora la pregunta que se hace todo el mundo: si Python convierte int a float sin pedírselo, ¿por qué no convierte "3" a 3?

print("3" + 4)
TypeError: can only concatenate str (not "int") to str

Porque sería ambiguo. "3" + 4 tiene dos interpretaciones igual de razonables:

  • Como suma: 3 + 47.
  • Como concatenación: "3" + "4""34".

Python no adivina. Es una decisión de diseño deliberada, y muy acertada: prefiere detenerse y avisarte a hacer algo distinto de lo que querías. Otros lenguajes sí adivinan, y esa comodidad les sale cara. En JavaScript, "3" + 4 da "34" y "3" - 4 da -1: la misma pareja de valores se comporta como texto con un operador y como número con otro, lo cual es una fuente inagotable de errores difíciles de encontrar.

La moraleja práctica: cuando veas un TypeError que menciona str e int, tienes un dato con el tipo equivocado. Casi siempre es un input() sin convertir.

flowchart TD
    A["Mezclo dos valores<br/>en una operacion"] --> B{"Ambos son<br/>numericos?"}
    B -->|"int y float"| C["Python convierte solo<br/>Resultado: float"]
    B -->|"str y numero"| D["TypeError<br/>Convierte tu con int(), float() o str()"]

  1. bool() y las reglas de veracidad

bool() convierte cualquier valor a True o False, y nunca falla. Pero sus reglas hay que conocerlas, porque no son obvias.

En Python, unos pocos valores se consideran falsos; todos los demás son verdaderos.

Valor bool(valor) Comentario
0 False El cero entero
0.0 False El cero decimal
"" False Cadena vacía
None False Ausencia de valor
False False Obviamente
" " True Un espacio no es vacío: tiene un carácter
"0" True Es un texto de un carácter, no el número cero
"False" True ¡Es un texto! Y no está vacío
-1 True Cualquier número distinto de cero
3.14 True Ídem

Las tres filas peligrosas son " ", "0" y "False". Las tres parecen que deberían dar False y las tres dan True, porque son textos no vacíos. bool() sobre un texto solo pregunta una cosa: ¿tiene al menos un carácter?

print(bool(""))          # False
print(bool(" "))         # True    <- cuidado
print(bool("False"))     # True    <- cuidado
print(bool("0"))         # True    <- cuidado

Esto tiene una aplicación directa e importantísima para nosotros: detectar si el usuario pulsó Intro sin escribir nada.

titulo = input("Titulo: ")           # el usuario pulsa Intro directamente

hay_titulo = bool(titulo)
print(hay_titulo)                    # False

Como input() devuelve "" cuando no se escribe nada, y "" es falso, bool(titulo) nos dice si hay algo. En la práctica se escribe más corto: el propio valor del texto ya sirve como condición, sin envolverlo en bool(). Es lo que Python llama valor de verdad de un objeto.

Pero atención a la trampa que se deriva de la tabla: si el usuario teclea solo espacios, titulo vale " ", que es verdadero. Un título de tres espacios pasaría el filtro. La solución es quitar los espacios antes de comprobar, y de eso trata el apartado siguiente.

Un último aviso: nunca uses bool() para interpretar una respuesta del usuario.

respuesta = input("Completada? (True/False): ")   # el usuario teclea False

print(bool(respuesta))     # True    <- NO es lo que esperabas

"False" es un texto no vacío, así que bool() da True. Para interpretar un sí/no hay que comparar el texto, no convertirlo:

completada = respuesta.strip().lower() == "si"

  1. Normalizar el texto: .strip(), .lower() y .capitalize()

Normalizar es transformar un dato a una forma canónica antes de compararlo o guardarlo, de modo que las diferencias irrelevantes —espacios sobrantes, mayúsculas— dejen de importar.

Python ofrece muchos métodos de cadena; los verás todos en Cadenas de caracteres. Aquí nos quedamos con los tres imprescindibles para sanear entradas.

Se llaman con un punto detrás de la variable de texto: variable.metodo().

.strip(): quitar espacios de los extremos

entrada = "  alta  "

print(entrada)              # '  alta  ' (con espacios)
print(entrada.strip())      # 'alta'

Elimina espacios, tabuladores y saltos de línea al principio y al final, pero no los de dentro:

print("  Panaderia  Sole  ".strip())     # 'Panaderia  Sole'

Es la primera operación que debe sufrir cualquier texto que venga de un teclado. Los espacios sobrantes son invisibles en pantalla y rompen todas las comparaciones.

.lower(): pasar todo a minúsculas

Aquí está, por fin, la solución al cabo suelto de la lección 01-05:

prioridad = "ALTA"

print(prioridad == "alta")             # False
print(prioridad.lower() == "alta")     # True

También existe .upper(), que hace lo contrario. La técnica se llama comparación insensible a mayúsculas, y consiste en llevar los dos lados a la misma caja antes de comparar. Como el literal "alta" ya está en minúsculas, basta con bajar el otro.

Lo normal es encadenar las dos operaciones. Los métodos se aplican de izquierda a derecha:

prioridad = input("Prioridad: ")        # el usuario teclea "  ALTA  "

prioridad = prioridad.strip().lower()

print(prioridad)                        # 'alta'
print(prioridad == "alta")              # True

Léelo así: al texto original se le quitan los espacios, y al resultado se le bajan las mayúsculas. Y fíjate en algo importante de la segunda línea: el resultado se reasigna a la propia variable. Si escribieras solo prioridad.strip().lower() sin el prioridad = delante, el resultado se calcularía y se tiraría, porque los métodos de cadena no modifican el texto original: devuelven uno nuevo. Es la inmutabilidad de las cadenas que viste en la lección 02-01, ahora con consecuencias prácticas.

prioridad = "  ALTA  "
prioridad.strip().lower()      # se calcula y se pierde
print(prioridad)               # '  ALTA  '  <- no ha cambiado nada

Este es uno de los errores más frecuentes en quien empieza. Recuérdalo: los métodos de cadena devuelven, no modifican.

.capitalize(): primera letra en mayúscula

Útil para nombres propios, que es exactamente el caso del responsable de una tarea:

print("nuria".capitalize())      # Nuria
print("NURIA".capitalize())      # Nuria
print("nUrIa".capitalize())      # Nuria

Pone la primera letra en mayúscula y el resto en minúscula. Eso último se olvida y a veces sorprende:

print("ana maria".capitalize())  # Ana maria   <- solo la primera palabra

Para poner en mayúscula la inicial de cada palabra existe .title(), con sus propias limitaciones. Como el equipo de Estudio Alba son tres nombres de una sola palabra, .capitalize() nos basta.

Método Qué hace " nUrIa "
.strip() Quita espacios de los extremos "nUrIa"
.lower() Todo a minúsculas " nuria "
.upper() Todo a mayúsculas " NURIA "
.capitalize() Primera letra mayúscula, resto minúscula " nuria "
.strip().capitalize() Encadenados "Nuria"

Fíjate en la anteúltima fila: .capitalize() sobre un texto que empieza por espacio no hace nada visible, porque la «primera letra» es un espacio. El .strip() va siempre primero.

  1. Comprobar antes de convertir: .isdigit() e .isalpha()

Ya sabemos que int("tres") detiene el programa. La forma de evitarlo sin recurrir todavía al manejo de errores es preguntar antes si la conversión es posible.

.isdigit() responde a: ¿este texto está compuesto solo por dígitos? Devuelve un booleano.

print("3".isdigit())         # True
print("300".isdigit())       # True
print("tres".isdigit())      # False
print("3.5".isdigit())       # False   <- el punto no es un digito
print("-5".isdigit())        # False   <- el signo tampoco
print("".isdigit())          # False   <- cadena vacia
print(" 3".isdigit())        # False   <- el espacio tampoco

Es una comprobación estricta, y esa estrictez es una virtud: si .isdigit() devuelve True, int() funcionará con seguridad. Pero ten presentes sus tres límites: no admite decimales, no admite negativos y no tolera espacios. Por eso el .strip() debe ir antes:

entrada = input("Dias estimados: ")     # el usuario teclea "  5  "

entrada = entrada.strip()
print(entrada.isdigit())                # True

.isalpha() es su equivalente para letras: ¿está compuesto solo por letras?

print("Luis".isalpha())          # True
print("Nuria3".isalpha())        # False
print("Luis Gomez".isalpha())    # False   <- el espacio no es una letra
print("".isalpha())              # False

Sirve para detectar que alguien ha escrito un número donde iba un nombre. Ojo con la tercera línea: un nombre compuesto contiene un espacio y no pasa el filtro, así que .isalpha() es adecuado para nombres de una sola palabra, como los del equipo de Estudio Alba, pero no para nombres y apellidos.

Método Devuelve True cuando... Casos que rechaza
.isdigit() Solo hay dígitos y hay al menos uno "", "3.5", "-5", "3 "
.isalpha() Solo hay letras y hay al menos una "", "Luis1", "Luis Gomez"
.isspace() Solo hay espacios en blanco "", cualquier texto con contenido

El patrón de uso seguro queda así:

1. Leer con input()
2. Limpiar con .strip()  (y .lower() si es una opcion de un conjunto)
3. Comprobar con .isdigit() / in (...) / longitud
4. Solo si la comprobacion pasa: convertir con int() o float()

Ese orden no es negociable: limpiar, comprobar, convertir.

  1. Validar contra una lista de valores permitidos

Cuando un dato solo puede tomar unos pocos valores —como la prioridad de TareaFácil, que según el requisito R2 solo admite alta, media y baja—, la comprobación se escribe con el operador in que aprendiste en la lección 02-02:

prioridad = input("Prioridad (alta/media/baja): ")
prioridad = prioridad.strip().lower()

prioridad_valida = prioridad in ("alta", "media", "baja")
print(prioridad_valida)

Con esa normalización previa, todas estas entradas se aceptan correctamente: alta, ALTA, Alta, alta , aLtA. Y estas se rechazan, como debe ser: urgente, altisima, `` (vacío), 1.

Lo mismo para el responsable, que según R3 debe ser una de las tres personas del estudio:

responsable = input("Responsable (Marta/Luis/Nuria): ")
responsable = responsable.strip().capitalize()

responsable_valido = responsable in ("Marta", "Luis", "Nuria")
print(responsable_valido)

Fíjate en la diferencia de normalización: para la prioridad usamos .lower(), porque los valores canónicos están en minúscula; para el responsable usamos .capitalize(), porque son nombres propios y queremos guardarlos bien escritos. La normalización debe llevar el dato a la forma en la que lo vas a almacenar, no a una forma cualquiera.

Ventajas de esta forma frente a encadenar comparaciones con or:

  • Se lee como la regla de negocio: «la prioridad debe estar entre estas tres».
  • Añadir un valor permitido es añadir un elemento, no otra comparación.
  • No se puede caer en la trampa de prioridad == "alta" or "media" que vimos en 02-02.

  1. Qué es validar y por qué se valida siempre

Recapitulemos con calma, porque este apartado es el corazón conceptual de la lección.

Validar es comprobar que un dato cumple las condiciones necesarias para poder usarse, antes de usarlo. No es una comprobación de tipo: es una comprobación de sentido.

Un dato puede fallar en cuatro niveles distintos, y conviene distinguirlos:

Nivel Pregunta Ejemplo de fallo Herramienta
Presencia ¿Hay algo? Título vacío .strip() y comprobar si queda algo
Tipo ¿Se puede convertir? Días = "tres" .isdigit(), luego int()
Dominio ¿Está entre los valores permitidos? Prioridad = "urgente" in ("alta", "media", "baja")
Coherencia ¿Tiene sentido? Días estimados = -5 o 9000 Comparaciones: 1 <= dias <= 365

Ese cuarto nivel se olvida siempre y es donde se cuelan los datos más absurdos. "-5" no pasaría .isdigit(), pero "0" sí, y una tarea de cero días estimados no significa nada. Convertir bien no basta: hay que preguntarse si el valor tiene sentido en el negocio.

¿Y por qué se valida todo lo que viene de fuera, siempre, sin excepción? Por tres razones de peso creciente:

  1. Robustez. Un programa que se detiene con ValueError porque alguien tecleó una letra de más es un programa que nadie querrá usar. Marta no debería tener que llamar a nadie porque el programa «ha petado».

  2. Integridad de los datos. Si TareaFácil acepta la prioridad "urgentisima", esa tarea nunca aparecerá al filtrar por prioridad alta, media o baja. El programa no fallará: mentirá, que es mucho peor. Un error visible se arregla; un dato corrupto se propaga durante meses.

  3. Seguridad. En programas conectados a bases de datos o a internet, los datos no validados son la puerta de entrada de los ataques más comunes del mundo. Está fuera del alcance de este curso, pero el hábito que estás construyendo ahora es exactamente el que evita esos problemas.

Y la regla de oro, que conviene aprenderse literalmente:

Nunca confíes en los datos que vienen de fuera del programa. Ni del teclado, ni de un fichero, ni de la red. Ni siquiera si quien los teclea eres tú.

Esa última coletilla no es retórica. El programador es el peor probador de su propio programa, porque teclea siempre lo que el programa espera. Marta no.

  1. Un if mínimo, y lo que aún nos falta

Hasta ahora hemos calculado booleanos (prioridad_valida, hay_titulo) y los hemos impreso. Pero validar sirve de poco si el programa no hace algo distinto según el resultado.

Para eso está el if, que se estudia a fondo en la próxima lección, Condicionales. Aquí lo usamos en su forma más simple, solo para que la validación tenga efecto visible:

prioridad = input("Prioridad (alta/media/baja): ")
prioridad = prioridad.strip().lower()

if prioridad in ("alta", "media", "baja"):
    print(f"Prioridad aceptada: {prioridad}")
else:
    print("Prioridad no valida. Se asigna 'media' por defecto.")
    prioridad = "media"

print(f"Prioridad final: {prioridad}")

Tres apuntes sobre la sintaxis, sin extendernos porque no es el tema de esta lección:

  • La línea del if termina en dos puntos (:).
  • Lo que va debajo está indentado (4 espacios), y esa indentación es la que le dice a Python qué instrucciones pertenecen a cada rama. En Python la indentación no es estética: es sintaxis.
  • else marca el camino alternativo, el que se toma cuando la condición es falsa.

Con esto, la validación ya no solo informa: actúa. Rechaza el valor incorrecto y pone uno por defecto.

Aun así, quedan dos limitaciones que este módulo no puede resolver:

No podemos volver a preguntar. Lo natural sería insistir hasta que el usuario escriba algo válido, tal y como diseñamos en el pseudocódigo de la lección 01-05 (aquel MIENTRAS prioridad_valida = FALSO). Repetir requiere un bucle, y los bucles llegan en Bucles; la aplicación completa a un menú de TareaFácil, en Menús interactivos. Por ahora, la estrategia es asignar un valor por defecto y avisar.

No podemos capturar el error de una conversión. Si el usuario teclea "tres" donde va un número, nuestra defensa es comprobar antes con .isdigit(). La otra vía —dejar que la conversión falle y atrapar el ValueError con try / except— es más potente y más habitual en código profesional, y se estudia en Depuración y manejo de errores.

Estrategia Cómo funciona Dónde se estudia
Comprobar antes (.isdigit()) Se evita el error preguntando primero Aquí
Valor por defecto (if / else) Se acepta una alternativa razonable Aquí, a fondo en 03-01
Volver a preguntar Se insiste hasta obtener un dato válido 03-02 y 03-04
Capturar el error (try / except) Se deja fallar y se gestiona la caída 08-02

Las cuatro son válidas y se combinan. Vas a acabar el curso usando las cuatro.

  1. TareaFácil: sanear y validar la entrada

Versión 0.5 de tareafacil.py. Retomamos la 0.4 de la lección anterior y añadimos lo aprendido: normalizamos los textos, convertimos los números que ahora sí podemos pedir por teclado, y validamos los cuatro niveles del apartado 9.

# tareafacil.py - Estudio Alba
# Version 0.5: entrada saneada, convertida y validada

NOMBRE_APP = "TareaFacil"
VERSION = "0.5"
ESTUDIO = "Estudio Alba"
ANCHO = 46
HORAS_POR_JORNADA = 8

PRIORIDADES = ("alta", "media", "baja")
EQUIPO = ("Marta", "Luis", "Nuria")
DIAS_POR_DEFECTO = 5

# --- Cabecera ---
print("=" * ANCHO)
print(f"{NOMBRE_APP + ' v' + VERSION:^{ANCHO}}")
print(f"{'Registro de tarea - ' + ESTUDIO:^{ANCHO}}")
print("=" * ANCHO)
print()

# ============================================================
# ENTRADA + SANEADO + VALIDACION
# ============================================================

# --- 1. Titulo: presencia ---
titulo = input("Titulo de la tarea          : ")
titulo = titulo.strip()

aviso_titulo = ""
if titulo == "":
    titulo = "(sin titulo)"
    aviso_titulo = "AVISO: el titulo estaba vacio."

# --- 2. Responsable: dominio ---
responsable = input("Responsable (Marta/Luis/Nuria): ")
responsable = responsable.strip().capitalize()

aviso_responsable = ""
if responsable not in EQUIPO:
    aviso_responsable = f"AVISO: '{responsable}' no es del equipo. Se asigna a Marta."
    responsable = "Marta"

# --- 3. Prioridad: dominio ---
prioridad = input("Prioridad (alta/media/baja) : ")
prioridad = prioridad.strip().lower()

aviso_prioridad = ""
if prioridad not in PRIORIDADES:
    aviso_prioridad = f"AVISO: '{prioridad}' no es una prioridad. Se asigna 'media'."
    prioridad = "media"

# --- 4. Dias estimados: tipo + coherencia ---
dias_texto = input("Dias estimados              : ")
dias_texto = dias_texto.strip()

aviso_dias = ""
if dias_texto.isdigit():
    dias_estimados = int(dias_texto)
    if dias_estimados < 1 or dias_estimados > 365:
        aviso_dias = f"AVISO: {dias_estimados} dias no es razonable. Se asignan {DIAS_POR_DEFECTO}."
        dias_estimados = DIAS_POR_DEFECTO
else:
    aviso_dias = f"AVISO: '{dias_texto}' no es un numero. Se asignan {DIAS_POR_DEFECTO} dias."
    dias_estimados = DIAS_POR_DEFECTO

# ============================================================
# PROCESO
# ============================================================

horas_estimadas = dias_estimados * HORAS_POR_JORNADA
completada = False
es_urgente = prioridad == "alta" and not completada

# ============================================================
# SALIDA
# ============================================================

print()
print("=" * ANCHO)
print(f"{'FICHA DE TAREA':^{ANCHO}}")
print("=" * ANCHO)
print(f"{'Titulo':<16}{titulo:>{ANCHO - 16}}")
print(f"{'Responsable':<16}{responsable:>{ANCHO - 16}}")
print(f"{'Prioridad':<16}{prioridad:>{ANCHO - 16}}")
print(f"{'Dias estimados':<16}{dias_estimados:>{ANCHO - 16}}")
print(f"{'Horas estimadas':<16}{horas_estimadas:>{ANCHO - 16}}")
print(f"{'Urgente':<16}{str(es_urgente):>{ANCHO - 16}}")
print("=" * ANCHO)

# --- Avisos de validacion ---
print()
print(aviso_titulo)
print(aviso_responsable)
print(aviso_prioridad)
print(aviso_dias)

Una ejecución con entradas «sucias» pero razonables, de las que Marta escribiría un lunes con prisa:

==============================================
                TareaFacil v0.5
        Registro de tarea - Estudio Alba
==============================================

Titulo de la tarea          :   Manual de marca Sole
Responsable (Marta/Luis/Nuria): LUIS
Prioridad (alta/media/baja) :   ALTA
Dias estimados              :  4

==============================================
                FICHA DE TAREA
==============================================
Titulo                    Manual de marca Sole
Responsable                               Luis
Prioridad                                 alta
Dias estimados                               4
Horas estimadas                             32
Urgente                                   True
==============================================

Las cuatro entradas venían con espacios o en mayúsculas, y las cuatro se han aceptado y guardado en su forma canónica. En la versión 0.4, esa misma ejecución habría producido Urgente: False, porque " ALTA " no era "alta". El cabo suelto de la lección 01-05 queda atado.

Y ahora una ejecución con entradas realmente erróneas:

Titulo de la tarea          :
Responsable (Marta/Luis/Nuria): Pedro
Prioridad (alta/media/baja) : urgentisima
Dias estimados              : cuatro

==============================================
                FICHA DE TAREA
==============================================
Titulo                            (sin titulo)
Responsable                              Marta
Prioridad                                media
Dias estimados                               5
Horas estimadas                             40
Urgente                                  False
==============================================

AVISO: el titulo estaba vacio.
AVISO: 'Pedro' no es del equipo. Se asigna a Marta.
AVISO: 'urgentisima' no es una prioridad. Se asigna 'media'.
AVISO: 'cuatro' no es un numero. Se asignan 5 dias.

El programa no se ha detenido en ningún momento. Antes, int("cuatro") habría lanzado un ValueError y la ejecución habría muerto ahí, sin ficha y sin explicación. Ahora cada dato incorrecto se detecta, se sustituye por algo razonable y se informa de lo ocurrido.

Repasemos las decisiones de diseño, porque son tan importantes como el código:

  • PRIORIDADES y EQUIPO son constantes en mayúsculas. Las listas de valores permitidos no deberían estar escritas dentro de la comprobación: puestas arriba, se ven de un vistazo y se cambian en un sitio.
  • El orden es siempre el mismo: leer → .strip() → normalizar → comprobar → convertir. No conviertas nunca antes de comprobar.
  • Cada dato usa la normalización que le corresponde: .lower() para la prioridad (valor de catálogo en minúsculas), .capitalize() para el responsable (nombre propio), solo .strip() para el título (es texto libre; bajarlo a minúsculas lo estropearía).
  • Los avisos se guardan en variables y se imprimen al final, para que la ficha salga limpia y los problemas queden agrupados. Cuando no hay error, la variable vale "" y se imprime una línea vacía.
  • Los días se validan en dos niveles: primero que sean un número (.isdigit()), después que el número tenga sentido (1 <= dias <= 365). Un 0 habría pasado el primer filtro y no el segundo.

Lo que sigue siendo mejorable

Sé honesto con esta versión, porque el módulo 3 empieza justo aquí:

Limitación Se resuelve en
Ante un dato incorrecto se pone un valor por defecto en vez de volver a preguntar Bucles
Los cuatro bloques de validación repiten la misma estructura Definición y uso de funciones
Solo se puede registrar una tarea por ejecución Menús interactivos y Listas
Al cerrar el programa, la tarea se pierde Guardar datos en archivos
Los avisos vacíos imprimen líneas en blanco Condicionales

Errores Comunes y Consejos

Olvidar convertir lo que devuelve input(). Es el error número uno del módulo. Si vas a calcular, convierte. Si el TypeError menciona str e int, mira tus input().

Convertir antes de comprobar. int(input(...)) es cómodo y frágil: cualquier letra tira el programa. El orden seguro es leer, limpiar, comprobar y solo entonces convertir.

Creer que int() redondea. int(3.9) es 3. Para redondear, round().

Esperar que int("3.0") funcione. No funciona: el punto no pertenece a un entero. Usa int(float("3.0")) si necesitas admitir esa forma.

Olvidar que los métodos de cadena no modifican. titulo.strip() sin asignar no hace nada. Tiene que ser titulo = titulo.strip().

Normalizar solo un lado de la comparación. prioridad.lower() == "Alta" sigue siendo False siempre. Si bajas un lado, el otro debe estar ya en minúsculas.

Aplicar .capitalize() antes de .strip(). " nuria".capitalize() no cambia nada visible, porque la primera «letra» es un espacio. El .strip() va primero.

Confiar en bool() para interpretar respuestas. bool("False") es True, y bool("0") también. Para sí/no, compara el texto normalizado.

Comprobar el vacío sin limpiar antes. titulo == "" no detecta un título de tres espacios. titulo.strip() == "" sí.

Usar .isdigit() con negativos o decimales. "-5" y "3.5" devuelven False. Si tu dato admite decimales o signo, .isdigit() no es la comprobación adecuada.

Validar solo el tipo y olvidar la coherencia. "0" y "99999" son dígitos perfectamente válidos, y ninguno de los dos es un número de días razonable. Pregúntate siempre si el valor tiene sentido en el problema, no solo si es convertible.

Consejo: normaliza en el momento de leer. Deja dato = input(...).strip() justo en la lectura, y a partir de ahí trabaja siempre con el dato limpio. Si la limpieza queda dispersa por el programa, tarde o temprano habrá un camino que se la salte.

Consejo: pon las opciones válidas en constantes. PRIORIDADES = ("alta", "media", "baja") arriba del fichero. La regla de negocio queda visible y se cambia en un solo sitio.

Consejo: prueba tu programa como si quisieras romperlo. Pulsa Intro sin escribir. Teclea espacios. Escribe en mayúsculas. Pon una letra donde va un número. Pon un número negativo. Cada cosa que rompa el programa es una validación que falta.

Ejercicios

Ejercicio 1: Predecir el resultado

Indica el resultado —o el error, con su nombre— de cada expresión, sin ejecutar nada.

a)  int("42")
b)  int("42.0")
c)  int(42.9)
d)  round(42.9)
e)  float("7")
f)  float("7,5")
g)  str(True) + " valor"
h)  bool("")
i)  bool(" ")
j)  bool("0")
k)  "  Alta  ".strip().lower()
l)  "  alta  ".lower().strip() == "alta"
m)  "LUIS".capitalize()
n)  "  luis".capitalize()
o)  "12".isdigit()
p)  "-12".isdigit()
q)  "3 dias".isdigit()
r)  "Nuria" in ("Marta", "Luis", "Nuria")
s)  "nuria" in ("Marta", "Luis", "Nuria")
t)  "3" + 4

Ejercicio 2: Sanear una entrada de horas

Escribe un programa registrar_horas.py que pida por teclado el nombre de una persona del equipo y las horas dedicadas a una tarea (un número entero), y que:

  1. Limpie ambas entradas de espacios sobrantes.
  2. Normalice el nombre a la forma Nombre (inicial mayúscula, resto minúsculas).
  3. Compruebe que el nombre está en el equipo (Marta, Luis, Nuria); si no, avise y asigne "Marta".
  4. Compruebe que las horas son un número entero entre 1 y 40; si no, avise y asigne 8.
  5. Calcule y muestre las jornadas completas de 8 horas y las horas sueltas (recuerda // y % de la lección 02-02).
  6. Muestre una ficha alineada con f-strings.

Prueba tu programa con estas cuatro entradas y comprueba que ninguna lo detiene: NURIA / 12, luego pedro / 8, luego Luis / ocho, y por último Luis / 100.

Ejercicio 3: Validar los cuatro niveles

Para cada uno de estos datos de TareaFácil, indica en qué nivel de validación falla (presencia, tipo, dominio o coherencia), qué herramienta de esta lección lo detecta y qué debería hacer el programa. Después escribe el fragmento de código que lo comprueba.

Dato Valor introducido
a Título: " "
b Prioridad: "URGENTE"
c Días estimados: "quince"
d Días estimados: "0"
e Responsable: "nuria"
f Horas dedicadas: "50" sobre 8 horas estimadas

Soluciones

Solución 1.

Expresión Resultado
a int("42") 42
b int("42.0") ValueError: el punto no pertenece a un entero
c int(42.9) 42 (trunca, no redondea)
d round(42.9) 43
e float("7") 7.0
f float("7,5") ValueError: la coma no es separador decimal
g str(True) + " valor" "True valor"
h bool("") False
i bool(" ") True — un espacio es un carácter
j bool("0") True — es un texto no vacío
k " Alta ".strip().lower() "alta"
l " alta ".lower().strip() == "alta" True — aquí el orden da igual, pero .strip() primero es el hábito correcto
m "LUIS".capitalize() "Luis"
n " luis".capitalize() " luis" — la primera «letra» es un espacio, no cambia nada
o "12".isdigit() True
p "-12".isdigit() False — el signo no es un dígito
q "3 dias".isdigit() False
r "Nuria" in ("Marta", "Luis", "Nuria") True
s "nuria" in ("Marta", "Luis", "Nuria") False — distingue mayúsculas; falta .capitalize()
t "3" + 4 TypeError

Las filas n y s son las que más se fallan, y las dos apuntan a lo mismo: normalizar mal es tan peligroso como no normalizar, porque da falsa sensación de seguridad.

Solución 2.

# registrar_horas.py - Estudio Alba

ANCHO = 40
EQUIPO = ("Marta", "Luis", "Nuria")
HORAS_POR_JORNADA = 8
HORAS_POR_DEFECTO = 8

print("=" * ANCHO)
print(f"{'REGISTRO DE HORAS':^{ANCHO}}")
print("=" * ANCHO)
print()

# --- Nombre: saneado y validacion de dominio ---
nombre = input("Persona (Marta/Luis/Nuria): ")
nombre = nombre.strip().capitalize()

aviso_nombre = ""
if nombre not in EQUIPO:
    aviso_nombre = f"AVISO: '{nombre}' no es del equipo. Se asigna a Marta."
    nombre = "Marta"

# --- Horas: validacion de tipo y de coherencia ---
horas_texto = input("Horas dedicadas (1-40)    : ")
horas_texto = horas_texto.strip()

aviso_horas = ""
if horas_texto.isdigit():
    horas = int(horas_texto)
    if horas < 1 or horas > 40:
        aviso_horas = f"AVISO: {horas} h esta fuera del rango 1-40. Se asignan {HORAS_POR_DEFECTO}."
        horas = HORAS_POR_DEFECTO
else:
    aviso_horas = f"AVISO: '{horas_texto}' no es un numero. Se asignan {HORAS_POR_DEFECTO} h."
    horas = HORAS_POR_DEFECTO

# --- Proceso ---
jornadas = horas // HORAS_POR_JORNADA
horas_sueltas = horas % HORAS_POR_JORNADA

# --- Salida ---
print()
print("-" * ANCHO)
print(f"{'Persona':<18}{nombre:>{ANCHO - 18}}")
print(f"{'Horas dedicadas':<18}{horas:>{ANCHO - 18}}")
print(f"{'Jornadas completas':<18}{jornadas:>{ANCHO - 18}}")
print(f"{'Horas sueltas':<18}{horas_sueltas:>{ANCHO - 18}}")
print("-" * ANCHO)
print(aviso_nombre)
print(aviso_horas)

Comportamiento con las cuatro entradas pedidas:

Entrada Nombre final Horas finales Jornadas Sueltas Aviso
NURIA / 12 Nuria 12 1 4 Ninguno
pedro / 8 Marta 8 1 0 Pedro no es del equipo
Luis / ocho Luis 8 1 0 'ocho' no es un número
Luis / 100 Luis 8 1 0 100 h fuera del rango

Ninguna de las cuatro detiene el programa. Fíjate en la primera fila: " NURIA " se convierte en "Nuria" gracias a .strip().capitalize(), y por eso pasa la validación de dominio. Sin ese saneado, Nuria —una persona real del equipo— habría sido rechazada.

Solución 3.

Dato Nivel Herramienta Qué hacer
a Título " " Presencia titulo.strip() == "" Rechazar y pedir de nuevo, o poner "(sin titulo)"
b Prioridad "URGENTE" Dominio .strip().lower() y luego in PRIORIDADES Rechazar y asignar "media" por defecto
c Días "quince" Tipo .isdigit() No convertir; avisar y usar el valor por defecto
d Días "0" Coherencia Comparación dias >= 1 Convertible y numérico, pero sin sentido: rechazar
e Responsable "nuria" Ninguno tras normalizar .strip().capitalize() Aceptar: es válido, solo estaba mal escrito
f Horas "50" sobre 8 estimadas Coherencia Comparación entre dos datos Avisar de desviación grave del presupuesto

Código de comprobación:

PRIORIDADES = ("alta", "media", "baja")
EQUIPO = ("Marta", "Luis", "Nuria")

# a) Presencia
titulo = "   ".strip()
titulo_ok = titulo != ""
print(titulo_ok)          # False

# b) Dominio
prioridad = "URGENTE".strip().lower()
prioridad_ok = prioridad in PRIORIDADES
print(prioridad_ok)       # False

# c) Tipo
dias_texto = "quince".strip()
dias_ok_tipo = dias_texto.isdigit()
print(dias_ok_tipo)       # False

# d) Coherencia
dias_texto = "0".strip()
dias_ok_tipo = dias_texto.isdigit()
print(dias_ok_tipo)       # True   <- pasa el filtro de tipo
dias = int(dias_texto)
dias_ok_rango = 1 <= dias <= 365
print(dias_ok_rango)      # False  <- pero no el de coherencia

# e) Dominio, tras normalizar
responsable = "nuria".strip().capitalize()
responsable_ok = responsable in EQUIPO
print(responsable)        # Nuria
print(responsable_ok)     # True

# f) Coherencia entre dos datos
horas_dedicadas = int("50".strip())
horas_estimadas = 8
desviacion_grave = horas_dedicadas > horas_estimadas * 2
print(desviacion_grave)   # True

Los casos d y e son los que enseñan de verdad. El d demuestra que pasar la validación de tipo no basta: "0" es un dígito perfectamente convertible y una tarea de cero días no significa nada. Y el e demuestra lo contrario: un dato que parecía inválido ("nuria" no está en EQUIPO) resulta ser perfectamente correcto en cuanto se normaliza. Validar sin normalizar antes rechaza datos buenos; normalizar sin validar después acepta datos malos. Hacen falta las dos cosas, y en ese orden.

Conclusión

Con esta lección se cierra el módulo 2, y se cierran también los dos cabos sueltos que lo abrieron.

Sabes convertir con int(), float() y str(), y conoces sus límites: int() trunca en vez de redondear, int("3.0") no funciona, float("3,5") tampoco. Sabes leer un ValueError —que te dice el valor exacto y la línea donde falló— y distinguirlo del TypeError, que aparece cuando el tipo no admite la operación. Entiendes que Python convierta int a float implícitamente pero se niegue a mezclar texto y número, y por qué esa negativa es una virtud del lenguaje y no un capricho.

Conoces las reglas de veracidad de bool() y sus tres trampas (" ", "0" y "False" son verdaderos). Normalizas el texto con .strip(), .lower() y .capitalize(), recordando que los métodos de cadena devuelven un texto nuevo y no modifican el original. Compruebas antes de convertir con .isdigit() e .isalpha(), y validas contra un catálogo de valores permitidos con in. Y tienes un marco mental para no olvidarte de nada: presencia, tipo, dominio y coherencia, en ese orden.

Sobre todo, has interiorizado el principio que sostiene todo lo anterior: nada de lo que viene de fuera del programa es de fiar. Ni del teclado, ni de un fichero, ni de quien lo escribe, aunque seas tú.

TareaFácil está en su versión 0.5 y ha dejado de ser frágil. Marta puede teclear ALTA , LUIS o Manual de marca Sole y el programa lo entiende; puede escribir urgentisima, Pedro o cuatro y el programa no se detiene: avisa, corrige y sigue. La comparación que fallaba en la prueba de escritorio de la lección 01-05 —aquel "ALTA" que no era "alta"— hoy funciona.

Repasa lo conseguido en este módulo: los datos de una tarea viven en variables con el tipo adecuado (02-01), se combinan en expresiones que calculan plazos, avances y urgencias (02-02), entran por teclado y salen en una ficha alineada con f-strings (02-03), y llegan saneados y validados (02-04). Eso es un programa que se usa, no un programa que se lee.

Y sin embargo lo has notado en cada apartado de esta lección: el programa toma decisiones a medias. Pone valores por defecto porque no sabe volver a preguntar. Registra una sola tarea y termina. Los if que hemos usado eran mínimos y prestados de la lección siguiente. Todo eso son estructuras de control: las instrucciones que permiten a un programa elegir caminos y repetir acciones, y que convierten una secuencia lineal en algo que reacciona.

Es justo lo que empieza en el módulo 3, con Condicionales, donde ese if que hemos usado de puntillas se explica a fondo —if, elif, else, condiciones anidadas, indentación— y TareaFácil pasará por fin de avisar de los problemas a decidir qué hacer con ellos.

Fundamentos de la Programación

Módulo 1: Introducción a la Programación

Módulo 2: Conceptos Básicos

Módulo 3: Estructuras de Control

Módulo 4: Funciones y Procedimientos

Módulo 5: Estructuras de Datos

Módulo 6: Algoritmos Básicos

Módulo 7: Objetos y Organización del Código

Módulo 8: Buenas Prácticas y Herramientas

Módulo 9: Proyecto Final y Cierre del Curso

© Copyright 2026. Todos los derechos reservados