Prueba técnica Python Developer Junior resuelta — Guía completa 2026

Las pruebas técnicas para desarrolladores Python junior son uno de los filtros más comunes en los procesos de selección de 2026. En este artículo resolvemos paso a paso una prueba técnica completa — con lógica, programación orientada a objetos, consumo de APIs, debugging y testing — explicando cada decisión de código para que no solo veas la solución, sino que entiendas el razonamiento detrás de ella.

¿Para quién es este artículo? Para estudiantes de Python que quieren prepararse para su primera entrevista técnica, y para cualquier persona que quiera ver cómo se resuelve un problema real de programación con buenas prácticas.

Ejercicio 1 — Lógica y manipulación de datos

El primer ejercicio evalúa cómo manejas listas de diccionarios y si sabes usar las herramientas de Python de forma idiomática. Tenemos esta lista de usuarios:



users = [ {"id": 1, "name": "Ana", "age": 25, "active": True}, {"id": 2, "name": "Luis", "age": 17, "active": False}, {"id": 3, "name": "Carlos", "age": 30, "active": True}, ]

Y debemos retornar: usuarios mayores de edad, usuarios activos, promedio de edad y el usuario más joven. La solución:



def
analyze_users(users: list) -> dict: """ Analiza una lista de usuarios y retorna estadísticas clave. Args: users: Lista de diccionarios con datos de usuario. Returns: Diccionario con adultos, activos, promedio de edad y más joven. """ if not users: return { "adults": [], "active_users": [], "average_age": 0, "youngest": None, } adults = [u for u in users if u["age"] >= 18] active = [u for u in users if u["active"]] avg_age = sum(u["age"] for u in users) / len(users) youngest = min(users, key=lambda u: u["age"]) return { "adults": adults, "active_users": active, "average_age": round(avg_age, 2), "youngest": youngest, } # --- Uso --- result = analyze_users(users) print("Adultos:", result["adults"]) print("Activos:", result["active_users"]) print("Promedio edad:", result["average_age"]) print("Más joven:", result["youngest"])

¿Por qué así? Usamos list comprehensions en lugar de bucles for con append() — es más legible y más rápido en Python. La función min() con key=lambda es la forma idiomática de encontrar el mínimo en una lista de diccionarios. Y validamos la lista vacía al inicio para evitar errores de división por cero en el promedio.

Ejercicio 2 — Programación orientada a objetos: BankAccount

Este ejercicio evalúa si entiendes encapsulamiento, validación de datos y manejo de excepciones. Una clase mal diseñada aquí es una red flag inmediata para cualquier entrevistador.



class
InsufficientFundsError(Exception): """Error personalizado para fondos insuficientes.""" pass class BankAccount: """ Cuenta bancaria con operaciones básicas de depósito, retiro y transferencia entre cuentas. """ def __init__(self, owner: str, balance: float = 0.0): if not owner or not isinstance(owner, str): raise ValueError("El nombre del titular no puede estar vacío.") if balance < 0: raise ValueError("El saldo inicial no puede ser negativo.") self.owner = owner self._balance = balance # atributo privado @property def balance(self) -> float: return self._balance def _validate_amount(self, amount: float) -> None: """Valida que el monto sea un número positivo.""" if not isinstance(amount, (int, float)): raise TypeError(f"El monto debe ser numérico, se recibió: {type(amount)}") if amount <= 0: raise ValueError("El monto debe ser mayor que cero.") def deposit(self, amount: float) -> float: """Deposita un monto en la cuenta. Retorna el nuevo saldo.""" self._validate_amount(amount) self._balance += amount print(f"✔ Depósito de ${amount:.2f} exitoso. Saldo: ${self._balance:.2f}") return self._balance def withdraw(self, amount: float) -> float: """Retira un monto de la cuenta. Retorna el nuevo saldo.""" self._validate_amount(amount) if amount > self._balance: raise InsufficientFundsError( f"Fondos insuficientes. Saldo: ${self._balance:.2f}, " f"solicitado: ${amount:.2f}" ) self._balance -= amount print(f"✔ Retiro de ${amount:.2f} exitoso. Saldo: ${self._balance:.2f}") return self._balance def transfer(self, other_account: "BankAccount", amount: float) -> None: """Transfiere un monto a otra cuenta BankAccount.""" if not isinstance(other_account, BankAccount): raise TypeError("La cuenta destino debe ser una instancia de BankAccount.") self.withdraw(amount) other_account.deposit(amount) print(f"✔ Transferencia de ${amount:.2f} a {other_account.owner} completada.") def __repr__(self) -> str: return f"BankAccount(owner='{self.owner}', balance={self._balance:.2f})" # --- Uso --- ana = BankAccount("Ana", balance=500) luis = BankAccount("Luis", balance=100) ana.deposit(200) ana.withdraw(100) ana.transfer(luis, 150) try: ana.withdraw(9999) except InsufficientFundsError as e: print(f"Error: {e}")

Puntos clave de esta solución: creamos una excepción personalizada InsufficientFundsError en lugar de usar un ValueError genérico — esto hace el manejo de errores más preciso. El balance usa un atributo privado _balance con una propiedad pública de solo lectura, evitando que se modifique directamente desde fuera. Y el método _validate_amount centraliza la validación para no repetir código en deposit y withdraw.

Ejercicio 3 — Consumo de API con manejo de errores

Consumir APIs es una habilidad fundamental para cualquier desarrollador backend. Lo que diferencia a un junior sólido no es saber hacer el request — es saber manejar todos los casos en que puede fallar.



import
requests from typing import Optional API_URL = "https://jsonplaceholder.typicode.com/users" def _parse_user(user: dict) -> dict: """Extrae solo los campos relevantes de un usuario.""" return { "name": user.get("name", "N/A"), "email": user.get("email", "N/A"), "company": user.get("company", {}).get("name", "N/A"), } def fetch_users(email: Optional[str] = None) -> list[dict]: """ Obtiene usuarios de la API y retorna nombre, email y empresa. Args: email: Si se proporciona, filtra por ese email exacto. Returns: Lista de usuarios con name, email y company. Raises: requests.exceptions.Timeout: Si la API no responde a tiempo. requests.exceptions.ConnectionError: Si no hay conexión. ValueError: Si la respuesta no es JSON válido. """ try: response = requests.get(API_URL, timeout=10) response.raise_for_status() # lanza error si status >= 400 data = response.json() if not isinstance(data, list): raise ValueError("La API no retornó una lista de usuarios.") users = [_parse_user(u) for u in data] if email: users = [u for u in users if u["email"].lower() == email.lower()] return users except requests.exceptions.Timeout: print("Error: La API tardó demasiado en responder.") return [] except requests.exceptions.ConnectionError: print("Error: No se pudo conectar a la API. Verifica tu conexión.") return [] except requests.exceptions.HTTPError as e: print(f"Error HTTP: {e}") return [] except (ValueError, KeyError) as e: print(f"Error al procesar la respuesta: {e}") return [] # --- Uso --- todos = fetch_users() print(f"Total usuarios: {len(todos)}") print(todos[0]) # Buscar por email resultado = fetch_users(email="Sincere@april.biz") print(resultado)

¿Por qué separar _parse_user? Porque las funciones pequeñas con una sola responsabilidad son más fáciles de testear, leer y mantener. Si la estructura de la API cambia, solo tocas esa función. Notar que usamos .get() en lugar de acceso directo con [] para evitar KeyError si algún campo viene vacío.

Ejercicio 4 — Debugging: encuentra y corrige el error

Este es el código con el bug:


# Código con error
def calculate_total(items): total = 0 for item in items: total += item["price"] * item["quantity"] return total products = [ {"name": "Laptop", "price": "1000", "quantity": 2}, # ← price es STRING {"name": "Mouse", "price": 50, "quantity": 1}, ]

¿Cuál es el error? El precio del Laptop está guardado como string "1000" en lugar de número 1000. Cuando Python intenta multiplicar "1000" * 2, no da un error inmediato — Python repite el string dos veces, resultando en "10001000", no en 2000. El resultado total sería completamente incorrecto y difícil de detectar porque no lanza una excepción.

La corrección:



# Código corregido
def calculate_total(items: list[dict]) -> float: """ Calcula el total de una lista de productos. Convierte price a float para manejar strings numéricos. """ if not items: return 0.0 total = 0.0 for item in items: try: price = float(item["price"]) # conversión explícita quantity = int(item["quantity"]) # conversión explícita if price < 0 or quantity < 0: raise ValueError(f"Valores negativos en: {item['name']}") total += price * quantity except (ValueError, KeyError) as e: print(f"Advertencia — producto ignorado: {e}") return round(total, 2) # ¿Cómo evitar que vuelva a pasar? # Opción A: validar los datos al recibirlos (dataclasses o Pydantic) # Opción B: definir el tipo en el esquema de la base de datos # Opción C: tests que cubran este caso con strings numéricos

Cómo evitar que vuelva a pasar: la solución más robusta para proyectos reales es usar Pydantic para validar la estructura de los datos de entrada. Si defines que price debe ser float, Pydantic lo convierte automáticamente si viene como string numérico, y lanza un error claro si no es convertible.

Ejercicio 5 — Testing con pytest

Escribir tests no es opcional — es lo que separa el código que "funciona en tu máquina" del código que funciona en producción. Aquí están los tests para la clase BankAccount:



# test_bank_account.py
import pytest from bank_account import BankAccount, InsufficientFundsError # ── Fixtures ────────────────────────────────────────────────── @pytest.fixture def account(): """Cuenta con saldo inicial de 100 para cada test.""" return BankAccount("Ana", balance=100.0) @pytest.fixture def empty_account(): """Cuenta sin saldo.""" return BankAccount("Luis") # ── Tests de depósito ───────────────────────────────────────── def test_deposit_increases_balance(account): account.deposit(50) assert account.balance == 150.0 def test_deposit_returns_new_balance(account): result = account.deposit(200) assert result == 300.0 def test_deposit_zero_raises_error(account): with pytest.raises(ValueError, match="mayor que cero"): account.deposit(0) def test_deposit_negative_raises_error(account): with pytest.raises(ValueError): account.deposit(-50) def test_deposit_string_raises_error(account): with pytest.raises(TypeError): account.deposit("100") # ── Tests de retiro ─────────────────────────────────────────── def test_withdraw_decreases_balance(account): account.withdraw(40) assert account.balance == 60.0 def test_withdraw_exact_balance(account): """Debe permitir retirar exactamente el saldo disponible.""" account.withdraw(100) assert account.balance == 0.0 def test_withdraw_insufficient_funds(account): with pytest.raises(InsufficientFundsError): account.withdraw(9999) def test_withdraw_from_empty_account(empty_account): with pytest.raises(InsufficientFundsError): empty_account.withdraw(1) def test_withdraw_negative_raises_error(account): with pytest.raises(ValueError): account.withdraw(-10) # ── Tests de transferencia ──────────────────────────────────── def test_transfer_moves_balance(account, empty_account): account.transfer(empty_account, 60) assert account.balance == 40.0 assert empty_account.balance == 60.0 def test_transfer_insufficient_funds(empty_account, account): with pytest.raises(InsufficientFundsError): empty_account.transfer(account, 50)

Para ejecutar los tests, instala pytest y corre el comando:


pip install pytest pytest test_bank_account.py -v

Buena práctica: los fixtures de pytest (@pytest.fixture) crean una instancia fresca de la clase antes de cada test, evitando que el estado de un test afecte al siguiente. Nunca compartas estado entre tests.

Resumen: qué hace destacar esta solución

Buena práctica Dónde se aplica
Excepciones personalizadas InsufficientFundsError en BankAccount
Validación de inputs _validate_amount(), listas vacías, tipos
Funciones pequeñas _parse_user(), _validate_amount()
Type hints Todos los métodos y funciones
Docstrings Todas las clases y funciones públicas
Manejo de errores de red Timeout, ConnectionError, HTTPError
Tests con fixtures Estado limpio por cada test con pytest

Si estás preparándote para una entrevista técnica de Python, practica resolviendo ejercicios como estos y luego revisa tu propio código con ojo crítico: ¿validé todos los inputs? ¿Manejé todos los errores posibles? ¿Mis funciones hacen una sola cosa? ¿Escribí al menos un test por caso de uso? Esas cuatro preguntas son el filtro que separa el código junior del código que realmente funciona en producción. Si tienes dudas sobre alguno de los ejercicios, déjalas en los comentarios.

Publicar un comentario

Post a Comment (0)

Artículo Anterior Artículo Anterior Artículo Siguiente Artículo Siguiente