Por qué Python sigue siendo el lenguaje favorito de los recruiters técnicos
Python aparece en más del 60 % de las ofertas de trabajo en data, backend y ML. Los entrevistadores lo usan porque permite evaluar algoritmia, diseño de software y productividad en el mismo ejercicio.
Esta guía cubre las 50 preguntas que aparecen con más frecuencia en entrevistas de empresas tech de LATAM, España y empresas globales en remoto. Cada pregunta incluye la respuesta que esperan, el código que tenés que poder escribir de memoria y los errores comunes que te hacen perder puntos.
Bloque 1: Fundamentos del lenguaje
1. ¿Qué es Python y cuáles son sus características principales?
Python es un lenguaje interpretado, dinámicamente tipado y multiparadigma (soporta programación imperativa, orientada a objetos y funcional). Sus pilares son:
- Sintaxis limpia que prioriza la legibilidad (PEP 8)
- Gestión automática de memoria con garbage collector
- Tipado dinámico fuerte (no implícito —
"3" + 3lanzaTypeError) - Ecosistema gigante: pip, PyPI, y miles de librerías
# Tipado dinámico: la variable puede cambiar de tipo
x = 42
x = "ahora soy string"
x = [1, 2, 3]2. ¿Cuál es la diferencia entre una lista, una tupla y un set?
Esta es la pregunta de calentamiento más común. Memorizá la tabla:
- list — mutable, ordenada, permite duplicados, acceso O(1) por índice
- tuple — inmutable, ordenada, permite duplicados, acceso O(1) por índice
- set — mutable, sin orden, sin duplicados, lookup O(1) promedio
lista = [1, 2, 2, 3] # permite duplicados
tupla = (1, 2, 2, 3) # inmutable
conjunto = {1, 2, 2, 3} # => {1, 2, 3} — elimina duplicados
# Cuándo usar cada uno:
# list → cuando necesitás orden y mutabilidad
# tuple → claves de diccionario, retorno múltiple de funciones, datos que no deben cambiar
# set → eliminar duplicados, operaciones de intersección/unión rápidas3. ¿Qué es un diccionario y cómo funciona internamente?
Un dict es una tabla hash. Las claves se hashean para determinar el bucket donde se almacena el valor. Desde Python 3.7 los dicts mantienen el orden de inserción.
agenda = {"nombre": "Ana", "edad": 30}
# Acceso seguro — nunca lances KeyError en producción
nombre = agenda.get("nombre", "desconocido")
# Iteración
for clave, valor in agenda.items():
print(f"{clave}: {valor}")
# Dict comprehension
cuadrados = {n: n**2 for n in range(1, 6)}
# => {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
# Merge en Python 3.9+
d1 = {"a": 1}
d2 = {"b": 2}
merged = d1 | d2 # => {"a": 1, "b": 2}Trampa de entrevista: ¿Qué tipos pueden ser claves de un dict? Solo los hashables (inmutables): strings, números, tuplas. Las listas NO pueden ser clave.
4. ¿Qué significa que Python sea "pass by object reference"?
Python no es pass-by-value ni pass-by-reference. Es pass-by-object-reference: la función recibe una referencia al objeto, no una copia.
def agregar(lista, elemento):
lista.append(elemento) # modifica el objeto original
lista = ["nueva lista"] # solo reasigna la variable local
mi_lista = [1, 2, 3]
agregar(mi_lista, 4)
print(mi_lista) # => [1, 2, 3, 4] — el append sí afectó el originalRegla: si el objeto es mutable (list, dict, set) y lo modificás sin reasignar, el cambio se ve afuera. Si reasignás la variable local, el original no cambia.
5. ¿Cuál es la diferencia entre `==` e `is`?
== compara valor. is compara identidad (si son el mismo objeto en memoria).
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True — mismo valor
print(a is b) # False — objetos distintos en memoria
print(a is c) # True — c apunta al mismo objeto que a
# Caso especial — integer caching (CPython)
x = 256
y = 256
print(x is y) # True — CPython cachea enteros -5 a 256
x = 257
y = 257
print(x is y) # False — fuera del rango cacheadoNunca uses is para comparar valores. Solo usalo para None: if x is None.
Bloque 2: Funciones y closures
6. ¿Qué es un argumento `*args` y `**kwargs`?
def log(*args, **kwargs):
"""
*args — tupla con todos los argumentos posicionales
**kwargs — dict con todos los argumentos con nombre
"""
print("args:", args)
print("kwargs:", kwargs)
log(1, 2, 3, usuario="Ana", nivel="INFO")
# args: (1, 2, 3)
# kwargs: {"usuario": "Ana", "nivel": "INFO"}
# También sirven para delegar:
def wrapper(*args, **kwargs):
print("antes")
resultado = funcion_real(*args, **kwargs)
print("después")
return resultado7. ¿Qué es una función lambda y cuándo usarla?
Una lambda es una función anónima de una sola expresión. Útil como argumento de sorted, map, filter.
doble = lambda x: x * 2
# Uso real: ordenar por criterio personalizado
personas = [{"nombre": "Carlos", "edad": 25}, {"nombre": "Ana", "edad": 30}]
personas.sort(key=lambda p: p["edad"])
# Con sorted (no modifica la lista original)
ordenadas = sorted(personas, key=lambda p: p["nombre"])Cuándo NO usar lambda: si necesitás más de una línea o querés reusar la función — usá def con nombre.
8. ¿Qué es un closure?
Un closure es una función que recuerda las variables del scope donde fue creada, aunque ese scope ya no exista.
def hacer_multiplicador(factor):
def multiplicar(numero):
return numero * factor # "factor" está en el closure
return multiplicar
por_tres = hacer_multiplicador(3)
por_cinco = hacer_multiplicador(5)
print(por_tres(10)) # => 30
print(por_cinco(10)) # => 50Closures son la base de los decoradores y del patrón factory en Python.
9. ¿Qué es un decorador y cómo se implementa?
Un decorador es una función que envuelve a otra función para modificar su comportamiento sin cambiarle el código.
import time
import functools
def medir_tiempo(func):
@functools.wraps(func) # preserva __name__, __doc__, etc.
def wrapper(*args, **kwargs):
inicio = time.perf_counter()
resultado = func(*args, **kwargs)
fin = time.perf_counter()
print(f"{func.__name__} tardó {fin - inicio:.4f}s")
return resultado
return wrapper
@medir_tiempo
def procesar(n):
return sum(range(n))
procesar(1_000_000)
# => procesar tardó 0.0312s
# Decorador con argumentos (fábrica de decoradores)
def reintentar(veces=3):
def decorador(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for intento in range(veces):
try:
return func(*args, **kwargs)
except Exception as e:
if intento == veces - 1:
raise
print(f"Intento {intento+1} fallido: {e}")
return wrapper
return decorador
@reintentar(veces=3)
def llamada_api():
pass10. ¿Cuál es la diferencia entre `@staticmethod` y `@classmethod`?
class Circulo:
PI = 3.14159
def __init__(self, radio):
self.radio = radio
def area(self): # método de instancia — recibe self
return self.PI * self.radio ** 2
@classmethod
def desde_diametro(cls, diametro): # método de clase — recibe cls
return cls(diametro / 2) # puede crear instancias
@staticmethod
def es_valido(radio): # método estático — no recibe nada especial
return radio > 0
c1 = Circulo(5)
c2 = Circulo.desde_diametro(10) # factory method
print(Circulo.es_valido(-1)) # FalseRegla práctica: @classmethod cuando necesitás acceder a la clase (factories, constructores alternativos). @staticmethod para funciones de utilidad que lógicamente pertenecen a la clase pero no necesitan estado.
Bloque 3: OOP
11. ¿Cómo funciona la herencia en Python?
class Animal:
def __init__(self, nombre):
self.nombre = nombre
def hablar(self):
raise NotImplementedError("Subclase debe implementar hablar()")
class Perro(Animal):
def hablar(self):
return f"{self.nombre} dice: Guau"
class Gato(Animal):
def hablar(self):
return f"{self.nombre} dice: Miau"
# Polimorfismo
animales = [Perro("Rex"), Gato("Luna")]
for a in animales:
print(a.hablar())
print(isinstance(animales[0], Animal)) # True
print(issubclass(Perro, Animal)) # True12. ¿Qué es el MRO (Method Resolution Order)?
El MRO determina en qué orden Python busca métodos en una jerarquía de herencia múltiple. Usa el algoritmo C3 Linearization.
class A:
def hola(self): return "A"
class B(A):
def hola(self): return "B"
class C(A):
def hola(self): return "C"
class D(B, C):
pass
# MRO: D -> B -> C -> A -> object
print(D().hola()) # => "B"
# super() sigue el MRO automáticamente
class E(B, C):
def hola(self):
return f"E -> {super().hola()}"
print(E().hola()) # => "E -> B"13. ¿Qué son los dunder methods (métodos mágicos)?
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __str__(self):
return f"({self.x}, {self.y})"
def __add__(self, otro):
return Vector(self.x + otro.x, self.y + otro.y)
def __len__(self):
return 2
def __eq__(self, otro):
return self.x == otro.x and self.y == otro.y
def __hash__(self):
return hash((self.x, self.y))
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # => (4, 6)
print(len(v1)) # => 214. ¿Qué es una dataclass?
from dataclasses import dataclass, field
from typing import List
@dataclass
class Producto:
nombre: str
precio: float
tags: List[str] = field(default_factory=list)
activo: bool = True
def con_descuento(self, pct: float) -> "Producto":
return Producto(self.nombre, self.precio * (1 - pct), self.tags)
p = Producto("Laptop", 999.99, ["tech", "trabajo"])
print(p)
# => Producto(nombre='Laptop', precio=999.99, tags=['tech', 'trabajo'], activo=True)
# frozen=True crea dataclasses inmutables (hashable)
@dataclass(frozen=True)
class Punto:
x: float
y: float15. ¿Cuál es la diferencia entre `__str__` y `__repr__`?
__repr__: representación inequívoca para desarrolladores. Idealmenteeval(repr(obj)) == obj.__str__: representación amigable para usuarios finales.
from datetime import datetime
d = datetime(2024, 1, 15)
print(repr(d)) # datetime.datetime(2024, 1, 15, 0, 0)
print(str(d)) # 2024-01-15 00:00:00
# En f-strings: !r usa repr, !s usa str (default)
print(f"{d!r}") # datetime.datetime(2024, 1, 15, 0, 0)Bloque 4: Iteradores, generadores y comprehensions
16. ¿Qué es un iterador y cómo se implementa?
class ContadorHasta:
def __init__(self, maximo):
self.maximo = maximo
self.actual = 0
def __iter__(self):
return self
def __next__(self):
if self.actual >= self.maximo:
raise StopIteration
self.actual += 1
return self.actual
for n in ContadorHasta(5):
print(n) # 1 2 3 4 517. ¿Qué es un generador y por qué es más eficiente?
Un generador usa yield en lugar de return. No carga todos los valores en memoria a la vez — los produce uno por uno.
# Lista: carga TODO en memoria
def numeros_lista(n):
return [i ** 2 for i in range(n)]
# Generador: produce de a uno (lazy)
def numeros_gen(n):
for i in range(n):
yield i ** 2
# Para n=1_000_000:
# lista — ~8 MB en memoria
# gen — ~200 bytes
gen = numeros_gen(5)
print(next(gen)) # 0
print(next(gen)) # 1
# Generator expression (igual que list comprehension pero con paréntesis)
cuadrados = (x**2 for x in range(1_000_000)) # lazy, muy eficiente18. ¿Qué hace `yield from`?
yield from delega la iteración a un sub-generador o iterable.
def aplanar(lista):
for elemento in lista:
if isinstance(elemento, list):
yield from aplanar(elemento)
else:
yield elemento
resultado = list(aplanar([1, [2, [3, 4]], 5]))
# => [1, 2, 3, 4, 5]19. ¿Cuál es la diferencia entre `map()`, `filter()` y list comprehensions?
numeros = [1, 2, 3, 4, 5, 6]
# map — aplica función a cada elemento (lazy)
dobles_map = list(map(lambda x: x * 2, numeros))
# comprehension — más pythónico y legible
dobles_comp = [x * 2 for x in numeros]
# filter — filtra elementos (lazy)
pares_filter = list(filter(lambda x: x % 2 == 0, numeros))
# comprehension con condición
pares_comp = [x for x in numeros if x % 2 == 0]
# Comprehension más compleja
resultado = [x**2 for x in range(10) if x % 2 == 0]
# => [0, 4, 16, 36, 64]Preferí comprehensions para casos simples. Usá map/filter cuando la función ya existe.
20. ¿Qué es `itertools` y para qué sirve?
import itertools
# chain — concatenar iterables sin copiar en memoria
combinado = list(itertools.chain([1, 2], [3, 4], [5]))
# => [1, 2, 3, 4, 5]
# product — producto cartesiano
for x, y in itertools.product("AB", [1, 2]):
print(x, y) # A1 A2 B1 B2
# islice — slice de un generador infinito
primeros_10 = list(itertools.islice(itertools.count(1), 10))
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# groupby — agrupar elementos consecutivos iguales
from itertools import groupby
datos = [("a", 1), ("a", 2), ("b", 3)]
for key, grupo in groupby(datos, key=lambda x: x[0]):
print(key, list(grupo))Bloque 5: Manejo de errores y context managers
21. ¿Cómo funciona el manejo de excepciones en Python?
def dividir(a, b):
try:
resultado = a / b
except ZeroDivisionError:
print("No se puede dividir por cero")
return None
except TypeError as e:
print(f"Tipos incorrectos: {e}")
raise # re-lanza la excepción original
else:
# se ejecuta SOLO si no hubo excepciones
return resultado
finally:
# se ejecuta SIEMPRE — ideal para limpiar recursos
print("Operación completada")
# Crear excepciones propias
class SaldoInsuficienteError(ValueError):
def __init__(self, saldo, monto):
super().__init__(f"Saldo {saldo} insuficiente para retirar {monto}")
self.saldo = saldo
self.monto = monto
# Capturar múltiples excepciones
try:
pass
except (ValueError, KeyError) as e:
print(f"Error: {e}")22. ¿Qué es un context manager y cómo se crea uno?
class ConexionDB:
def __enter__(self):
print("Conectando...")
self.conexion = "conexion_simulada"
return self.conexion
def __exit__(self, tipo_exc, valor_exc, traceback):
print("Desconectando...")
return False # propaga la excepción si hubo
with ConexionDB() as conn:
print(f"Usando: {conn}")
# Implementación con contextlib (más simple)
from contextlib import contextmanager
@contextmanager
def tempdir():
import tempfile, shutil
directorio = tempfile.mkdtemp()
try:
yield directorio
finally:
shutil.rmtree(directorio)
with tempdir() as d:
print(f"Trabajando en {d}")
# el directorio se borra automáticamenteBloque 6: Programación funcional
23. ¿Qué son `zip()`, `enumerate()` y `any()`/`all()`?
nombres = ["Ana", "Carlos", "María"]
edades = [25, 30, 28]
# zip — combina iterables en pares
for nombre, edad in zip(nombres, edades):
print(f"{nombre}: {edad}")
# Crear dict desde dos listas
agenda = dict(zip(nombres, edades))
# enumerate — índice + valor
for i, nombre in enumerate(nombres, start=1):
print(f"{i}. {nombre}")
# any / all
numeros = [1, 3, 5, 4, 7]
print(any(n % 2 == 0 for n in numeros)) # True
print(all(n > 0 for n in numeros)) # True24. ¿Qué es `functools.reduce()`?
from functools import reduce
numeros = [1, 2, 3, 4, 5]
producto = reduce(lambda acc, x: acc * x, numeros)
# => 120 (1 * 2 * 3 * 4 * 5)
# Con valor inicial
suma = reduce(lambda acc, x: acc + x, numeros, 100)
# => 11525. ¿Qué es `functools.lru_cache`?
lru_cache memoiza los resultados de una función para evitar recálculos costosos.
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Sin cache: O(2^n) — con cache: O(n)
print(fibonacci(100)) # instantáneo
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
# Python 3.9+ tiene cache sin límite
from functools import cache
@cache
def factorial(n):
return n * factorial(n - 1) if n else 1Bloque 7: Tipado estático y type hints
26. ¿Qué son los type hints y cómo se usan?
from typing import Optional, List, Dict, Tuple
def procesar(
nombres: List[str],
config: Optional[Dict[str, int]] = None,
) -> Tuple[int, List[str]]:
if config is None:
config = {}
filtrados = [n for n in nombres if len(n) > config.get("min_len", 0)]
return len(filtrados), filtrados
# Python 3.10+ — sintaxis más limpia
def validar(valor: int | str | None) -> bool:
return valor is not None
# TypeVar para funciones genéricas
from typing import TypeVar
T = TypeVar("T")
def primero(lista: List[T]) -> Optional[T]:
return lista[0] if lista else None27. ¿Qué es un Protocol en Python?
Los Protocol permiten duck typing estructural — definir interfaces sin herencia explícita.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializable(Protocol):
def to_dict(self) -> dict: ...
def to_json(self) -> str: ...
class Usuario:
def __init__(self, nombre: str):
self.nombre = nombre
def to_dict(self) -> dict:
return {"nombre": self.nombre}
def to_json(self) -> str:
import json
return json.dumps(self.to_dict())
# Usuario no hereda de Serializable, pero lo cumple
def guardar(obj: Serializable) -> None:
print(obj.to_json())
guardar(Usuario("Ana"))
print(isinstance(Usuario("x"), Serializable)) # TrueBloque 8: Async/await y concurrencia
28. ¿Cuál es la diferencia entre threading, multiprocessing y asyncio?
- threading — I/O-bound, librerías síncronas. El GIL bloquea CPU-bound.
- multiprocessing — CPU-bound (cálculo pesado). Overhead de fork, memoria por proceso.
- asyncio — I/O-bound con alta concurrencia. Requiere código async; no hay paralelismo real.
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
return await response.json()
async def main():
urls = ["https://api.ejemplo.com/1", "https://api.ejemplo.com/2"]
async with aiohttp.ClientSession() as session:
resultados = await asyncio.gather(*[fetch(session, u) for u in urls])
return resultados
asyncio.run(main())29. ¿Qué es el GIL y cómo afecta al código Python?
El Global Interpreter Lock (GIL) es un mutex que permite que solo un thread ejecute bytecode Python a la vez en CPython.
import multiprocessing
def calcular(n):
return sum(range(n))
# Multiprocessing — SÍ paraleliza CPU-bound (procesos separados, sin GIL compartido)
with multiprocessing.Pool(4) as pool:
resultados = pool.map(calcular, [10**7] * 4)
# concurrent.futures — API más simple
from concurrent.futures import ProcessPoolExecutor
with ProcessPoolExecutor(max_workers=4) as executor:
futuros = [executor.submit(calcular, 10**7) for _ in range(4)]
resultados = [f.result() for f in futuros]30. ¿Cómo funcionan las coroutines y `async/await`?
import asyncio
async def tarea(nombre, segundos):
print(f"{nombre} iniciando")
await asyncio.sleep(segundos) # cede el control al event loop
print(f"{nombre} terminó")
return f"resultado de {nombre}"
async def main():
# Secuencial: 3 segundos total
# r1 = await tarea("A", 1)
# r2 = await tarea("B", 2)
# Concurrente: ~2 segundos (el máximo)
r1, r2 = await asyncio.gather(
tarea("A", 1),
tarea("B", 2),
)
print(r1, r2)
asyncio.run(main())Bloque 9: Módulos y paquetes
31. ¿Cuál es la diferencia entre un módulo y un paquete?
- Módulo: cualquier archivo
.py - Paquete: directorio con
__init__.py - Paquete namespace: directorio sin
__init__.py(Python 3.3+)
# Estructura de paquete
# myapp/
# __init__.py
# models/
# __init__.py
# usuario.py
# utils.py
import myapp.utils
from myapp.models.usuario import Usuario
from myapp.models import usuario as m
# En __init__.py podés controlar qué se exporta:
# from .models.usuario import Usuario
# __all__ = ["Usuario"]32. ¿Qué hace `if __name__ == "__main__"`?
def sumar(a, b):
return a + b
# Este bloque SOLO se ejecuta cuando el archivo es el punto de entrada
# NO se ejecuta cuando el archivo se importa como módulo
if __name__ == "__main__":
print(sumar(3, 4)) # 7Bloque 10: Estructuras de datos y algoritmos
33. ¿Cómo invertís una lista sin usar `reverse()`?
lista = [1, 2, 3, 4, 5]
# Slicing — crea nueva lista
invertida = lista[::-1]
# reversed() — iterador lazy, O(1) espacio
for x in reversed(lista):
print(x)
# In-place sin reverse() — O(n) tiempo, O(1) espacio
izq, der = 0, len(lista) - 1
while izq < der:
lista[izq], lista[der] = lista[der], lista[izq]
izq += 1
der -= 134. ¿Cómo encontrás el elemento más frecuente en una lista?
from collections import Counter
datos = [1, 3, 2, 1, 3, 1, 4, 3, 2, 1]
conteo = Counter(datos)
mas_frecuente = conteo.most_common(1)[0][0]
print(mas_frecuente) # 1
print(conteo.most_common(3)) # [(1, 4), (3, 3), (2, 2)]
# Manual (si te lo piden sin Counter)
def mas_frecuente_manual(lista):
freq = {}
for x in lista:
freq[x] = freq.get(x, 0) + 1
return max(freq, key=freq.get)35. ¿Cómo implementás una pila y una cola en Python?
from collections import deque
# Pila (LIFO) — lista
pila = []
pila.append(1)
pila.append(2)
tope = pila.pop() # O(1)
# Cola (FIFO) — deque, NO lista (list.pop(0) es O(n))
cola = deque()
cola.append(1)
cola.append(2)
frente = cola.popleft() # O(1)
# Cola de prioridad
import heapq
min_heap = []
heapq.heappush(min_heap, 3)
heapq.heappush(min_heap, 1)
heapq.heappush(min_heap, 2)
print(heapq.heappop(min_heap)) # 1 — siempre el menor36. ¿Cómo implementás una búsqueda binaria?
def busqueda_binaria(arr, objetivo):
izq, der = 0, len(arr) - 1
while izq <= der:
mid = (izq + der) // 2
if arr[mid] == objetivo:
return mid
elif arr[mid] < objetivo:
izq = mid + 1
else:
der = mid - 1
return -1
arr = [1, 3, 5, 7, 9, 11, 13]
print(busqueda_binaria(arr, 7)) # => 3
print(busqueda_binaria(arr, 4)) # => -1
# Python tiene bisect en stdlib
import bisect
pos = bisect.bisect_left(arr, 7)
print(pos) # 337. ¿Cómo detectás si una cadena es palíndromo?
def es_palindromo(s: str) -> bool:
# Normalizar: minúsculas, solo alfanuméricos
s = "".join(c.lower() for c in s if c.isalnum())
return s == s[::-1]
# Con dos punteros — O(n) tiempo, O(1) espacio extra
def es_palindromo_v2(s: str) -> bool:
s = "".join(c.lower() for c in s if c.isalnum())
izq, der = 0, len(s) - 1
while izq < der:
if s[izq] != s[der]:
return False
izq += 1
der -= 1
return True
print(es_palindromo("A man a plan a canal Panama")) # True
print(es_palindromo("race a car")) # FalseBloque 11: Testing
38. ¿Cómo escribís tests con `pytest`?
import pytest
def sumar(a, b):
return a + b
def test_sumar():
assert sumar(2, 3) == 5
assert sumar(-1, 1) == 0
@pytest.mark.parametrize("a,b,esperado", [
(2, 3, 5),
(-1, 1, 0),
(100, -50, 50),
])
def test_sumar_parametrizado(a, b, esperado):
assert sumar(a, b) == esperado
def dividir(a, b):
if b == 0:
raise ValueError("División por cero")
return a / b
def test_dividir_por_cero():
with pytest.raises(ValueError, match="División por cero"):
dividir(10, 0)
@pytest.fixture
def usuario_base():
return {"nombre": "Ana", "saldo": 1000}
def test_usuario(usuario_base):
assert usuario_base["saldo"] == 100039. ¿Qué es mocking y cuándo se usa?
from unittest.mock import patch, MagicMock
def obtener_precio_externo(producto_id: str) -> float:
import requests
r = requests.get(f"https://api.precios.com/{producto_id}")
return r.json()["precio"]
def calcular_total(producto_id: str, cantidad: int) -> float:
precio = obtener_precio_externo(producto_id)
return precio * cantidad
# Test sin tocar la API real
@patch("__main__.obtener_precio_externo")
def test_calcular_total(mock_precio):
mock_precio.return_value = 10.0
assert calcular_total("ABC", 3) == 30.0
mock_precio.assert_called_once_with("ABC")
# Mock de objetos
mock_db = MagicMock()
mock_db.query.return_value = [{"id": 1}]
resultado = mock_db.query("SELECT *")
print(resultado) # [{"id": 1}]Bloque 12: Patrones avanzados
40. ¿Qué es el patrón Singleton en Python?
class SingletonMeta(type):
_instancias = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instancias:
cls._instancias[cls] = super().__call__(*args, **kwargs)
return cls._instancias[cls]
class ConfiguracionApp(metaclass=SingletonMeta):
def __init__(self):
self.debug = False
self.max_conexiones = 10
c1 = ConfiguracionApp()
c2 = ConfiguracionApp()
print(c1 is c2) # True — misma instancia41. ¿Cómo implementás el patrón Observer?
from typing import Callable, List
class EventEmitter:
def __init__(self):
self._listeners: dict[str, List[Callable]] = {}
def on(self, evento: str, callback: Callable):
self._listeners.setdefault(evento, []).append(callback)
return self
def emit(self, evento: str, *args, **kwargs):
for callback in self._listeners.get(evento, []):
callback(*args, **kwargs)
def off(self, evento: str, callback: Callable):
self._listeners.get(evento, []).remove(callback)
emitter = EventEmitter()
emitter.on("usuario.creado", lambda u: print(f"Bienvenido {u}"))
emitter.on("usuario.creado", lambda u: print(f"Email enviado a {u}"))
emitter.emit("usuario.creado", "Ana")42. ¿Qué es una metaclass en Python?
Una metaclass es "la clase de una clase" — define cómo se comportan las clases mismas.
class ValidadorAtributos(type):
def __new__(mcs, nombre, bases, namespace):
for attr in namespace:
if not attr.startswith("_") and not attr.isupper():
raise ValueError(f"El atributo {attr!r} debe estar en MAYÚSCULAS")
return super().__new__(mcs, nombre, bases, namespace)
class Constantes(metaclass=ValidadorAtributos):
PI = 3.14159
E = 2.71828
# Uso más común: ABCs
from abc import ABC, abstractmethod
class Figura(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimetro(self) -> float: ...43. ¿Qué es un descriptor en Python?
class Tipado:
def __init__(self, tipo):
self.tipo = tipo
self.nombre = None
def __set_name__(self, owner, nombre):
self.nombre = nombre
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.nombre)
def __set__(self, obj, valor):
if not isinstance(valor, self.tipo):
raise TypeError(
f"{self.nombre} debe ser {self.tipo.__name__}, "
f"got {type(valor).__name__}"
)
obj.__dict__[self.nombre] = valor
class Persona:
nombre = Tipado(str)
edad = Tipado(int)
def __init__(self, nombre, edad):
self.nombre = nombre
self.edad = edad
p = Persona("Ana", 30)
# p.edad = "treinta" # lanza TypeErrorBloque 13: Rendimiento y optimización
44. ¿Cómo medís y optimizás el rendimiento de Python?
import timeit
# Comparar concatenación vs join
t_join = timeit.timeit(
stmt='"".join(palabras)',
setup='palabras = ["hola"] * 1000',
number=1000,
)
# join es ~10x más rápido que concatenación con +
# cProfile — profiling completo
import cProfile
cProfile.run("funcion_lenta()")
# __slots__ reduce memoria en clases con muchas instancias
class PuntoOptimizado:
__slots__ = ("x", "y") # evita __dict__ por instancia
def __init__(self, x, y):
self.x = x
self.y = y
# List comprehension > loop append (más rápido y más legible)
cuadrados = [x**2 for x in range(1000)]45. ¿Cuándo usás `numpy` sobre listas?
import numpy as np
arr = np.arange(1_000_000)
# Suma de cuadrados
# Python puro: ~200ms | NumPy: ~2ms
resultado = np.sum(arr ** 2)
# Broadcasting — operaciones sin loops explícitos
a = np.array([[1, 2, 3], [4, 5, 6]]) # shape (2, 3)
b = np.array([10, 20, 30]) # shape (3,)
c = a + b # broadcast: shape (2, 3)
# Indexing booleano
datos = np.array([1, -2, 3, -4, 5])
positivos = datos[datos > 0] # => [1, 3, 5]Bloque 14: Pythonismos y best practices
46. ¿Cuáles son los principales principios de código pythónico?
# Intercambio sin variable temporal
a, b = b, a
# Unpacking extendido
primero, *resto = [1, 2, 3, 4, 5]
*inicio, ultimo = [1, 2, 3, 4, 5]
primero, *medio, ultimo = [1, 2, 3, 4, 5]
# Walrus operator := (Python 3.8+)
import re
texto = "hay 42 errores"
if m := re.search(r"\d+", texto):
print(m.group()) # "42"
# EAFP — más pythónico que chequear antes
try:
valor = diccionario["key"]
except KeyError:
valor = default
# Concatenación eficiente de strings
partes = ["Hola", "mundo", "Python"]
resultado = " ".join(partes)47. ¿Qué es PEP 8 y cuáles son sus reglas más importantes?
# Nombres
mi_variable = 1 # snake_case para variables y funciones
MiClase = True # PascalCase para clases
MI_CONSTANTE = 42 # UPPER_SNAKE para constantes
_privado = True # _ prefix para atributos privados (convención)
# Espaciado correcto
x = 1 + 2
def funcion(a, b=0): # sin espacio en default args
pass
# Líneas largas: usar paréntesis para continuación
resultado = (
muy_largo_nombre_variable
+ otro_nombre_variable
+ un_tercero
)
# Imports al inicio, uno por línea por módulo estándar
import os
import sys
from typing import List, Optional # múltiples del mismo: ok48. ¿Cómo usás `collections.defaultdict` y `collections.namedtuple`?
from collections import defaultdict, namedtuple
# defaultdict — evita KeyError
palabras = "el gato come el pescado el gato".split()
conteo = defaultdict(int)
for palabra in palabras:
conteo[palabra] += 1
agrupado = defaultdict(list)
datos = [("A", 1), ("B", 2), ("A", 3), ("B", 4)]
for k, v in datos:
agrupado[k].append(v)
# => {"A": [1, 3], "B": [2, 4]}
# namedtuple — tupla con nombres de campos (inmutable, eficiente)
Punto = namedtuple("Punto", ["x", "y"])
Empleado = namedtuple("Empleado", "nombre edad departamento")
p = Punto(3, 4)
print(p.x, p.y) # acceso por nombre
print(p[0]) # acceso por índice también funciona
x, y = p # unpacking también funciona49. ¿Qué son las variables de entorno y cómo se manejan en Python?
import os
from pathlib import Path
# Leer variable de entorno
db_url = os.environ.get("DATABASE_URL", "sqlite:///local.db")
debug = os.environ.get("DEBUG", "false").lower() == "true"
# python-dotenv — cargar .env automáticamente
from dotenv import load_dotenv
load_dotenv()
# pathlib — manejo moderno de rutas
ruta = Path("/home/usuario/proyecto")
archivo = ruta / "datos" / "config.json"
if archivo.exists():
contenido = archivo.read_text(encoding="utf-8")
# Iterar archivos
for py_file in ruta.glob("**/*.py"):
print(py_file)
# Crear directorio de forma segura
(ruta / "output").mkdir(parents=True, exist_ok=True)50. ¿Cómo estructurás un proyecto Python profesional?
La estructura recomendada usa src layout con pyproject.toml:
mi_proyecto/
├── src/
│ └── mi_paquete/
│ ├── __init__.py
│ ├── models.py
│ ├── services.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ ├── test_models.py
│ └── test_services.py
├── pyproject.toml
├── .env.example
└── README.md# pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mi-paquete"
version = "0.1.0"
dependencies = [
"requests>=2.31",
"pydantic>=2.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.mypy]
strict = trueCómo usar esta guía para preparar tu entrevista
Semana antes: repasá los bloques de fundamentos (1-5), funciones (6-10) y OOP (11-15).
Día anterior: bloques de generadores, async/await y algoritmos (16-37).
La mañana de la entrevista: repasá los pythonismos (46-50) — son los que los entrevistadores esperan que domines naturalmente.
En la entrevista: cuando recibas un ejercicio, pensá en voz alta. Los entrevistadores evalúan tu proceso tanto como la solución final. La práctica más efectiva es escribir el código de memoria, no leerlo — cerrá esta página y reescribí las funciones que te resultaron menos claras.