Nivel: Intermedio
Tema: Aleatoriedad, reglas de validación, secrets vs random, composición de strings, tests con pytest
Objetivo: Generar contraseñas que cumplan reglas mínimas (longitud y diversidad de caracteres) usando prácticas seguras.

Enunciado
Crea una función llamada generar_contrasena(longitud=12, usar_mayusculas=True, usar_numeros=True, usar_simbolos=True) que:
- Reciba:
longitud(int): longitud total de la contraseña- flags booleanos para incluir tipos de caracteres
- Reglas de validación:
- Si
longitudesNoneo no esint, lanceTypeError - Si
longitud< 4, lanceValueError - Si alguno de los flags no es
bool, lanceTypeError - Si todos los flags (
usar_mayusculas,usar_numeros,usar_simbolos) sonFalse, igualmente debe generar una contraseña válida usando solo minúsculas (esto sigue siendo válido)
- Si
- La contraseña debe:
- Tener exactamente
longitudcaracteres - Contener al menos 1 minúscula siempre
- Si
usar_mayusculasesTrue, contener al menos 1 mayúscula - Si
usar_numerosesTrue, contener al menos 1 número - Si
usar_simbolosesTrue, contener al menos 1 símbolo
- Tener exactamente
- Usar el módulo
secrets(norandom) para elegir caracteres.
Debe devolver un str con la contraseña.
Ejemplos
generar_contrasena(12)→"aB3!k..."generar_contrasena(8, usar_simbolos=False)→ contiene minúsculas, mayúsculas y números, pero sin símbolosgenerar_contrasena(10, False, False, False)→ solo minúsculas, longitud 10generar_contrasena(3)→ ValueErrorgenerar_contrasena("12")→ TypeError
Pistas
- Define conjuntos de caracteres:
- minúsculas:
string.ascii_lowercase - mayúsculas:
string.ascii_uppercase - números:
string.digits - símbolos: por ejemplo
"!@#$%^&*()-_=+[]{}"
- minúsculas:
- Para garantizar reglas, construye primero una lista con los caracteres “obligatorios” y luego completa el resto.
- Mezcla el resultado con
secrets.SystemRandom().shuffle()o seleccionando posiciones aleatorias (más simple: shuffle conSystemRandom).
Solución explicada (paso a paso)
- Validamos tipos y rango de
longitud. - Validamos que los flags sean booleanos.
- Construimos:
- una lista
obligatorioscon 1 carácter de cada tipo requerido - un pool
permitidoscon todos los caracteres habilitados
- una lista
- Completamos la contraseña eligiendo caracteres al azar desde
permitidoshasta alcanzarlongitud. - Mezclamos el orden para que los “obligatorios” no queden siempre al inicio.
- Convertimos la lista en string y devolvemos.
Python
import secretsimport stringimport randomdef generar_contrasena( longitud: int = 12, usar_mayusculas: bool = True, usar_numeros: bool = True, usar_simbolos: bool = True,) -> str: """ Genera una contraseña con reglas mínimas y aleatoriedad segura (secrets). Reglas: - longitud debe ser int y >= 4 - flags deben ser bool - siempre incluye al menos 1 minúscula - incluye al menos 1 de cada tipo habilitado """ if longitud is None or not isinstance(longitud, int): raise TypeError("El parámetro 'longitud' debe ser un entero (int).") if longitud < 4: raise ValueError("La 'longitud' mínima es 4.") for flag, nombre in [ (usar_mayusculas, "usar_mayusculas"), (usar_numeros, "usar_numeros"), (usar_simbolos, "usar_simbolos"), ]: if not isinstance(flag, bool): raise TypeError(f"El parámetro '{nombre}' debe ser booleano (bool).") minusculas = string.ascii_lowercase mayusculas = string.ascii_uppercase numeros = string.digits simbolos = "!@#$%^&*()-_=+[]{}" obligatorios = [secrets.choice(minusculas)] permitidos = list(minusculas) if usar_mayusculas: obligatorios.append(secrets.choice(mayusculas)) permitidos.extend(mayusculas) if usar_numeros: obligatorios.append(secrets.choice(numeros)) permitidos.extend(numeros) if usar_simbolos: obligatorios.append(secrets.choice(simbolos)) permitidos.extend(simbolos) # Si la suma de obligatorios ya excede la longitud, no se puede cumplir la regla. if len(obligatorios) > longitud: raise ValueError("La longitud es demasiado corta para cumplir todas las reglas seleccionadas.") restantes = longitud - len(obligatorios) for _ in range(restantes): obligatorios.append(secrets.choice(permitidos)) # Mezclar el orden (SystemRandom usa fuente segura del sistema) random.SystemRandom().shuffle(obligatorios) return "".join(obligatorios)
Python
import pytestfrom reto_19_generador_contrasenas import generar_contrasenadef tiene_minuscula(s: str) -> bool: return any(c.islower() for c in s)def tiene_mayuscula(s: str) -> bool: return any(c.isupper() for c in s)def tiene_numero(s: str) -> bool: return any(c.isdigit() for c in s)def tiene_simbolo(s: str) -> bool: simbolos = set("!@#$%^&*()-_=+[]{}") return any(c in simbolos for c in s)def test_longitud_correcta(): pwd = generar_contrasena(12) assert isinstance(pwd, str) assert len(pwd) == 12def test_reglas_por_defecto(): pwd = generar_contrasena(12) assert tiene_minuscula(pwd) assert tiene_mayuscula(pwd) assert tiene_numero(pwd) assert tiene_simbolo(pwd)def test_sin_simbolos(): pwd = generar_contrasena(12, usar_simbolos=False) assert tiene_minuscula(pwd) assert tiene_mayuscula(pwd) assert tiene_numero(pwd) assert not tiene_simbolo(pwd)def test_solo_minusculas(): pwd = generar_contrasena(10, False, False, False) assert len(pwd) == 10 assert tiene_minuscula(pwd) assert not tiene_mayuscula(pwd) assert not tiene_numero(pwd) assert not tiene_simbolo(pwd)def test_longitud_minima(): with pytest.raises(ValueError): generar_contrasena(3)def test_tipo_incorrecto_longitud(): with pytest.raises(TypeError): generar_contrasena("12")def test_flag_no_bool(): with pytest.raises(TypeError): generar_contrasena(12, usar_mayusculas="si")
Ejecuta:
pytest -q
Variantes para subir de nivel (opcional)
- Excluir caracteres ambiguos (O/0, l/1) para contraseñas “human-friendly”
- Permitir configuración del set de símbolos
- Generar múltiples contraseñas en una llamada
- Añadir parámetro
semilla(solo para modo demo, no para producción)
Lo que aprendiste
- Por qué
secretses preferible arandompara contraseñas - Cómo garantizar reglas mínimas de composición
- Cómo testear propiedades (no valores exactos) cuando hay aleatoriedad
- Diseño robusto con validación de entradas
Accede al código completo y a los tests en GitHub para ejecutar y modificar la solución localmente.
Continúa con Reto #20 — Manejo robusto de errores (try/except bien hecho) para escribir funciones resistentes y con mensajes de error útiles.