SolveConPython

Python Reto #19 — Generador de contraseñas (con reglas)

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.

Reto #19 Generador de contraseñas (con reglas)
Reto #19 Generador de contraseñas (con reglas)

Enunciado

Crea una función llamada generar_contrasena(longitud=12, usar_mayusculas=True, usar_numeros=True, usar_simbolos=True) que:

  1. Reciba:
    • longitud (int): longitud total de la contraseña
    • flags booleanos para incluir tipos de caracteres
  2. Reglas de validación:
    • Si longitud es None o no es int, lance TypeError
    • Si longitud < 4, lance ValueError
    • Si alguno de los flags no es bool, lance TypeError
    • Si todos los flags (usar_mayusculas, usar_numeros, usar_simbolos) son False, igualmente debe generar una contraseña válida usando solo minúsculas (esto sigue siendo válido)
  3. La contraseña debe:
    • Tener exactamente longitud caracteres
    • Contener al menos 1 minúscula siempre
    • Si usar_mayusculas es True, contener al menos 1 mayúscula
    • Si usar_numeros es True, contener al menos 1 número
    • Si usar_simbolos es True, contener al menos 1 símbolo
  4. Usar el módulo secrets (no random) 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ímbolos
  • generar_contrasena(10, False, False, False) → solo minúsculas, longitud 10
  • generar_contrasena(3)ValueError
  • generar_contrasena("12")TypeError

Pistas

  1. Define conjuntos de caracteres:
    • minúsculas: string.ascii_lowercase
    • mayúsculas: string.ascii_uppercase
    • números: string.digits
    • símbolos: por ejemplo "!@#$%^&*()-_=+[]{}"
  2. Para garantizar reglas, construye primero una lista con los caracteres “obligatorios” y luego completa el resto.
  3. Mezcla el resultado con secrets.SystemRandom().shuffle() o seleccionando posiciones aleatorias (más simple: shuffle con SystemRandom).

Solución explicada (paso a paso)

  1. Validamos tipos y rango de longitud.
  2. Validamos que los flags sean booleanos.
  3. Construimos:
    • una lista obligatorios con 1 carácter de cada tipo requerido
    • un pool permitidos con todos los caracteres habilitados
  4. Completamos la contraseña eligiendo caracteres al azar desde permitidos hasta alcanzar longitud.
  5. Mezclamos el orden para que los “obligatorios” no queden siempre al inicio.
  6. Convertimos la lista en string y devolvemos.

Python
import secrets
import string
import random
def 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 pytest
from reto_19_generador_contrasenas import generar_contrasena
def 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) == 12
def 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)

  1. Excluir caracteres ambiguos (O/0, l/1) para contraseñas “human-friendly”
  2. Permitir configuración del set de símbolos
  3. Generar múltiples contraseñas en una llamada
  4. Añadir parámetro semilla (solo para modo demo, no para producción)

Lo que aprendiste

  • Por qué secrets es preferible a random para 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.