1¿Qué tipos de datos incorporados tiene Python y cuáles son mutables?
Python tiene tipos escalares como NoneType, bool, int, float y complex; tipos de texto y binarios como str, bytes y bytearray; secuencias como list, tuple y range; y colecciones como dict, set y frozenset. La diferencia importante en una entrevista es la mutabilidad: list, dict, set y bytearray pueden cambiar in place, mientras que números, bool, str, bytes, tuple, range, frozenset y None son inmutables. Un matiz clave es que las referencias almacenadas por un tuple son fijas, pero el estado interno de un objeto mutable referenciado puede cambiar.
2¿En qué se diferencian list, tuple, set y dict en código real?
list es una secuencia ordenada y mutable, útil para colas de trabajo, resultados acumulados y datos ordenados que cambian. tuple es ordenado pero inmutable, así que encaja con registros fijos, retornos de funciones y valores que no deberían reasignarse. set almacena objetos hashable únicos y es ideal para membership checks o deduplicación. dict asigna claves hashable a valores y es la estructura habitual para índices, lookups y datos parecidos a JSON. En Python moderno, dict conserva el orden de inserción, pero su objetivo principal sigue siendo el acceso por clave.
items = ["a", "b", "a"]
unique = set(items)
index = {"user_id": 42}
point = (10, 20)
== pregunta si dos objetos son iguales por valor, normalmente mediante __eq__. is pregunta si dos referencias apuntan exactamente al mismo objeto en memoria. En production code, is suele reservarse para comprobaciones de singletons como value is None, value is True solo cuando la identidad realmente importa, o sentinel objects. No conviene depender de detalles de implementación como el caché de enteros pequeños o el interning de strings: pueden hacer que is parezca funcionar en ejemplos aunque sea la semántica incorrecta.
Un objeto hashable tiene un valor hash estable durante su vida y un comportamiento de igualdad coherente con ese hash. Importa porque las claves de dict y los elementos de set dependen de ello. La mayoría de built-ins inmutables son hashable, pero no todos los objetos dentro de un contenedor inmutable se vuelven hashable automáticamente: un tuple que contiene una list no es hashable. En entrevistas, conecta esto con diseño: si un objeto se usa como clave, los campos que participan en __eq__ o __hash__ no deben cambiar después de insertarlo, o el objeto puede volverse inalcanzable dentro de un dict o set.
5¿Cómo funcionan los slices, incluidos índices negativos y pasos?
Un slice sequence[start:stop:step] incluye start, excluye stop y avanza con step. Los límites omitidos se infieren según la dirección del paso. Los índices negativos cuentan desde el final, así que s[-1] es el último elemento. Un step negativo recorre de derecha a izquierda; s[::-1] crea una secuencia invertida para muchos tipos de secuencia. El trade-off es que un slice suele crear un objeto nuevo, por lo que es expresivo pero no siempre gratis para datos grandes.
text = "interview"
print(text[1:5])
print(text[::-1])
Python pasa referencias a objetos por asignación, a menudo llamado call by sharing. La función recibe nombres de parámetros locales ligados a los mismos objetos que entregó el caller. Si el objeto es mutable, la función puede mutarlo y el caller observará el cambio. Si la función reasigna el nombre del parámetro, solo cambia la vinculación local. Una buena respuesta distingue mutación in place de rebinding local: items.append(...) o items.clear() cambia la lista del caller, mientras que items = [] dentro de la función solo apunta el nombre local a una lista nueva.
7¿Por qué son peligrosos los argumentos mutables por defecto?
Los valores por defecto se evalúan una sola vez cuando se define la función, no en cada llamada. Si el default es una list o un dict, todas las llamadas pueden compartir el mismo objeto y filtrar estado accidentalmente. El patrón seguro es def f(items=None): y crear items = [] dentro cuando items is None. Hay casos raros donde el estado compartido es intencional, como un caché pequeño, pero entonces debe ser explícito y documentado porque sorprende a quien lee el código.
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
print(add_tag("python"))
print(add_tag("backend")) # Same list reused
def safe_add_tag(tag, tags=None):
tags = [] if tags is None else tags
tags.append(tag)
return tags
*args recoge argumentos posicionales extra en un tuple, mientras que **kwargs recoge argumentos nombrados extra en un dict. Son útiles para wrappers, decorators, APIs flexibles y forwarding de llamadas. En el call site, *iterable expande valores posicionales y **mapping expande keyword arguments. El trade-off es la claridad: los parámetros explícitos son mejores cuando la función tiene un contrato público estable, mientras args y kwargs encajan mejor en límites donde importan forwarding o compatibilidad.
Python busca nombres en Local, Enclosing, Global y Builtins, en ese orden. Leer un nombre sigue esa secuencia. Asignar un nombre dentro de una función normalmente crea una vinculación local, por eso el código puede lanzar UnboundLocalError si lee el nombre antes de una asignación local. global redirige la asignación al scope del módulo, y nonlocal la redirige a una función envolvente. En production code conviene usarlos con moderación porque hacen menos obvio el flujo de estado.
total = 0
def add(value):
global total
total += value
def counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
Un closure es una función que conserva acceso a variables de un scope externo después de que la función externa haya terminado. Es útil para decorators, callback factories y funciones configuradas. Late binding significa que la variable libre se busca cuando se llama la función interna, no cuando se crea. Lambdas creadas en un loop pueden ver todas el valor final del contador. Captura el valor actual con un argumento por defecto, por ejemplo lambda i=i: i, o usa una pequeña función factory.
callbacks = []
for i in range(3):
callbacks.append(lambda: i)
print([callback() for callback in callbacks]) # [2, 2, 2]
callbacks = []
for i in range(3):
callbacks.append(lambda i=i: i)
print([callback() for callback in callbacks]) # [0, 1, 2]
11¿En qué se diferencian las variables de clase y de instancia?
Una variable de clase se guarda en el objeto clase y se comparte entre instancias salvo que una instancia la oculte con su propio atributo. Una variable de instancia se guarda en un objeto concreto, normalmente asignada como self.name en __init__. Las constantes de clase inmutables suelen estar bien, pero los atributos mutables de clase son una fuente común de bugs cuando se usan por accidente como estado por instancia. En una entrevista, menciona el orden de búsqueda: Python mira primero la instancia, luego la clase y sus ancestros.
class Cart:
items = []
first = Cart()
second = Cart()
first.items.append("book")
print(second.items) # ["book"]
class SafeCart:
def __init__(self):
self.items = []
Python resuelve atributos mediante el Method Resolution Order, visible con ClassName.__mro__. En herencia simple es directo, pero con herencia múltiple Python usa la linearización C3 para crear un orden consistente. super() no significa exactamente 'llamar a mi padre'; significa 'llamar a la siguiente implementación en el MRO'. Eso permite herencia múltiple cooperativa cuando los métodos aceptan argumentos compatibles y cada clase llama correctamente a super().
class A:
def save(self):
print("A")
class B(A):
def save(self):
print("B")
super().save()
class C(A):
def save(self):
print("C")
super().save()
class D(B, C):
pass
D().save() # B, C, A
13¿Cuál es la diferencia entre iterable, iterator y generator?
Un iterable es cualquier objeto que puede devolver un iterator mediante iter(), como una list o un file object. Un iterator es el objeto con estado que implementa __next__ y lanza StopIteration cuando se agota. Un generator es una forma cómoda de crear un iterator con yield o con una generator expression. Los generators son eficientes en memoria porque producen valores de forma lazy, pero normalmente son one-shot y no pueden indexarse como una list.
def read_lines(path):
with open(path, encoding="utf-8") as file:
for line in file:
yield line.strip()
14¿Qué es un context manager y por qué es útil with?
Un context manager define comportamiento de entrada y salida con __enter__ y __exit__, o puede crearse con contextlib.contextmanager. La sentencia with hace explícita y fiable la gestión de recursos: se cierran archivos, se liberan locks y las transacciones pueden hacer commit o rollback incluso si hay excepciones. __exit__ recibe información de la excepción y puede suprimirla devolviendo true, aunque suprimir errores debe hacerse con cuidado.
from contextlib import contextmanager
with open("data.txt", encoding="utf-8") as file:
data = file.read()
@contextmanager
def temporary_setting():
print("enter")
try:
yield
finally:
print("exit")
15¿En qué se diferencian shallow copy y deep copy?
Una shallow copy crea un nuevo contenedor externo pero mantiene referencias a los mismos objetos anidados. Una deep copy copia recursivamente los objetos anidados cuando es posible. Para listas planas simples, una shallow copy suele bastar. Para estructuras mutables anidadas, una shallow copy puede compartir estado y sorprenderte. Deep copy es más cara y puede fallar con TypeError o PicklingError para recursos como archivos abiertos, sockets, sesiones de base de datos u objetos con semántica sensible a la identidad.
from copy import copy, deepcopy
original = [[1], [2]]
shallow = copy(original)
deep = deepcopy(original)
original[0].append(99)
print(shallow) # [[1, 99], [2]]
print(deep) # [[1], [2]]
En CPython estándar, el Global Interpreter Lock permite que solo un thread ejecute bytecode de Python en un momento dado. Esto significa que los threads normalmente no aceleran código Python CPU-bound. Aun así, los threads son útiles para trabajo I/O-bound porque el GIL se libera durante muchas operaciones de espera. Para trabajo CPU-heavy se usan multiprocessing, native extensions, bibliotecas nativas vectorizadas o servicios separados. En entrevistas de 2026 también conviene mencionar los builds free-threaded de CPython en Python 3.13+: hacen que el GIL sea opcional para código compatible, pero requieren comprobar dependencias y performance.
17¿Cuándo conviene elegir threads, processes o asyncio?
Usa threads cuando tienes blocking I/O o librerías que solo exponen APIs síncronas. Usa processes para trabajo CPU-bound, aislamiento o contención de crashes, aceptando overhead de serialización y memoria. Usa asyncio cuando tienes muchas operaciones concurrentes de I/O y librerías compatibles con async, como drivers async de base de datos o clientes HTTP. La elección práctica también depende del framework y de las dependencias: mezclar async code con llamadas bloqueantes puede empeorar el rendimiento.
18¿Para qué sirve async/await en servicios web Python?
async y await habilitan concurrencia cooperativa. Una coroutine devuelve el control al event loop cuando espera I/O, permitiendo que otras tareas avancen. Esto es valioso para APIs que esperan base de datos, llamadas HTTP, queues o sockets. No vuelve más rápido el código CPU-bound: el trabajo CPU-heavy bloquea el event loop salvo que se mueva a un worker, process o código nativo. En FastAPI, un endpoint normal def se ejecuta en un thread pool, mientras que llamadas bloqueantes dentro de async def pueden bloquear el event loop; async ayuda solo si el call path usa I/O async no bloqueante.
19¿Cómo se deben manejar excepciones en Python de producción?
Captura la excepción más específica que puedas manejar, y hazlo en una capa que pueda recuperarse, añadir contexto o traducirla a un domain error. Evita bare except y evita tragarte errores en silencio porque eso oculta pérdida de datos y problemas operativos. Usa raise para relanzar la misma excepción y raise NewError(...) from exc para conservar el contexto causal. El buen código de producción también registra logs en los límites, devuelve errores HTTP adecuados y mantiene mensajes útiles sin filtrar secrets.
try:
result = parse_payload(payload)
except ValueError as exc:
raise InvalidRequest("Invalid payload") from exc
20¿Cómo estructurarías y probarías un servicio Python de producción?
Separa la lógica de dominio del framework y del código de infraestructura para que el comportamiento central pueda probarse sin servidor ni base de datos. Usa configuración desde environment o typed settings, logging en lugar de print, dependency injection en los límites y timeouts explícitos para llamadas de red. En tests, combina unit tests para lógica pura, integration tests para límites con base de datos o sistemas externos, y algunos end-to-end checks para flujos críticos. Mockea sistemas externos en límites claros, pero prueba comportamiento observable en vez de detalles privados de implementación.