InterviewHack.ai
Empezar gratis
Blog/Preguntas de entrevista de Python — 50 con código y respuestas

Preguntas de entrevista de Python — 50 con código y respuestas

15 de septiembre de 2026

pythonentrevistas

Las 50 preguntas de entrevista de Python más frecuentes, con respuestas detalladas y código real. Desde tipos de datos hasta decoradores, generadores, async/await y design patterns. El recurso más completo para preparar tu próxima entrevista técnica.

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" + 3 lanza TypeError)
  • Ecosistema gigante: pip, PyPI, y miles de librerías
python
# 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
python
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ápidas

3. ¿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.

python
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.

python
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 original

Regla: 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).

python
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 cacheado

Nunca 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`?

python
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 resultado

7. ¿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.

python
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.

python
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))  # => 50

Closures 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.

python
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():
    pass

10. ¿Cuál es la diferencia entre `@staticmethod` y `@classmethod`?

python
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))            # False

Regla 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?

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))        # True

12. ¿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.

python
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)?

python
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))   # => 2

14. ¿Qué es una dataclass?

python
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: float

15. ¿Cuál es la diferencia entre `__str__` y `__repr__`?

  • __repr__: representación inequívoca para desarrolladores. Idealmente eval(repr(obj)) == obj.
  • __str__: representación amigable para usuarios finales.
python
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?

python
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 5

17. ¿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.

python
# 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 eficiente

18. ¿Qué hace `yield from`?

yield from delega la iteración a un sub-generador o iterable.

python
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?

python
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?

python
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?

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?

python
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áticamente

Bloque 6: Programación funcional

23. ¿Qué son `zip()`, `enumerate()` y `any()`/`all()`?

python
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))         # True

24. ¿Qué es `functools.reduce()`?

python
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)
# => 115

25. ¿Qué es `functools.lru_cache`?

lru_cache memoiza los resultados de una función para evitar recálculos costosos.

python
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 1

Bloque 7: Tipado estático y type hints

26. ¿Qué son los type hints y cómo se usan?

python
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 None

27. ¿Qué es un Protocol en Python?

Los Protocol permiten duck typing estructural — definir interfaces sin herencia explícita.

python
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))  # True

Bloque 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.
python
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.

python
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`?

python
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+)
python
# 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__"`?

python
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))   # 7

Bloque 10: Estructuras de datos y algoritmos

33. ¿Cómo invertís una lista sin usar `reverse()`?

python
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 -= 1

34. ¿Cómo encontrás el elemento más frecuente en una lista?

python
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?

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 menor

36. ¿Cómo implementás una búsqueda binaria?

python
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)  # 3

37. ¿Cómo detectás si una cadena es palíndromo?

python
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"))                    # False

Bloque 11: Testing

38. ¿Cómo escribís tests con `pytest`?

python
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"] == 1000

39. ¿Qué es mocking y cuándo se usa?

python
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?

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 instancia

41. ¿Cómo implementás el patrón Observer?

python
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.

python
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?

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 TypeError

Bloque 13: Rendimiento y optimización

44. ¿Cómo medís y optimizás el rendimiento de Python?

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?

python
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?

python
# 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?

python
# 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: ok

48. ¿Cómo usás `collections.defaultdict` y `collections.namedtuple`?

python
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 funciona

49. ¿Qué son las variables de entorno y cómo se manejan en Python?

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
toml
# 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 = true

Có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.

FAQ

¿Cuáles son las preguntas más frecuentes en entrevistas de Python?+

Las más frecuentes son: diferencia entre lista, tupla y set; cómo funciona el GIL; qué son los generadores y cómo se diferencian de las listas; decoradores y closures; manejo de excepciones; y preguntas de algoritmia como búsqueda binaria o detectar palíndromos.

¿Necesito saber async/await para una entrevista de Python backend?+

Depende del rol. Para roles backend con FastAPI, Django Channels o cualquier stack moderno, sí es esperado. Para roles de data science o scripting, es menos crítico. Al menos debés poder explicar la diferencia entre threading, multiprocessing y asyncio.

¿Me preguntan sobre el GIL en entrevistas técnicas?+

Sí, especialmente en roles de backend o infraestructura. El concepto clave es: el GIL impide el paralelismo real de threads en CPU-bound tasks, por eso se usa multiprocessing para cálculo pesado y threading o asyncio para I/O-bound.

¿Qué nivel de Python se evalúa en entrevistas de empresas tech internacionales?+

Esperan que domines fluentemente los fundamentos (tipos de datos, OOP, funciones de orden superior), que conozcas la stdlib (collections, itertools, functools), y que puedas escribir código pythónico sin pensar. Los generadores, decoradores y type hints son diferenciales.

¿Tengo que memorizar la complejidad algorítmica de las operaciones de Python?+

Sí, las más importantes: list.append() es O(1) amortizado, list.pop(0) es O(n) (usá deque), dict/set lookup es O(1) promedio. Conocer cuándo usar qué estructura de datos es tan importante como el algoritmo en sí.

¿Qué es lo más importante para aprobar una entrevista técnica de Python?+

Tres cosas: escribir código limpio y legible (PEP 8, nombres claros), pensar en voz alta mostrando tu razonamiento, y conocer las estructuras de datos de la stdlib. Los entrevistadores prefieren candidatos que reconocen cuándo usar Counter, defaultdict o heapq antes que reimplementarlos desde cero.

Artículos relacionados

Cómo responder 'cuál es tu mayor debilidad' en entrevistas tech

Descubre consejos reales para responder 'cuál es tu mayor debilidad' sin clichés en entrevistas tech, aplicando estrategias útiles y ejemplos concretos.

Cómo prepararte para una entrevista de Desarrollador Frontend

Descubre consejos prácticos para preparar tu entrevista de Desarrollador Frontend. Domina técnicas, retos y preguntas en roles remotos y en inglés.

Cómo prepararte para una entrevista de Desarrollador Backend

Guía experta con tips y ejemplos para superar entrevistas backend. Prepárate para roles remotos en tech, con consejos prácticos y foco en LATAM.

Cómo prepararte para una entrevista de Ingeniero de Datos

Aprende a prepararte para una entrevista de Ingeniero de Datos: skills técnicas, preguntas comunes, ejemplos de desafíos y tips para roles remotos/dólar.

Preparate para tu entrevista real

Pegá el link de tu vacante: investigamos quién te entrevista y te ensayamos en vivo.

Empezar gratis →

¿Tenés entrevista próxima? Instalá el copiloto en vivo →

InterviewHack.ai

Preparate para la entrevista exacta: quién te entrevista, tu CV a medida y coach real.

Producto

VacantesRevisar CV (ATS) gratis¿Cómo suena tu inglés?¿Te pagan bien?Reporte de sueldos LATAMCursos gratisBlogCV a medidaPráctica habladaEs gratis

Empleos remotos

ReactPythonFull-StackLATAMArgentinaMéxicoVer todas →

Preparate

Práctica habladaFrontendBackendAI EngineerPor empresaVendete con tu CV

Empresa

Buscás talentoAcerca deContactoPrivacidadTérminos

© 2026 InterviewHack.ai · Tu CV es tuyo. Nunca se usa para entrenar nada. · Un producto de IA-PTY