Errores de Argumentos Predeterminados Mutables
Los valores predeterminados de los argumentos se evalúan una vez en el momento de la definición de la función. Los valores predeterminados mutables (list, dict, set) se convierten en estado compartido entre llamadas, produciendo contaminación entre solicitudes y fallos "a veces incorrectos" en las pruebas.
Receta
Tarjeta de receta de referencia rápida, lista para copiar y pegar.
# ERROR
def append_item(item, bucket=[]):
bucket.append(item)
return bucket
# SOLUCIÓN
def append_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucketCuándo usar esto:
- Los manejadores de API acumulan elementos entre solicitudes no relacionadas
- Las pruebas unitarias pasan solas pero fallan cuando se ejecutan como un conjunto
- Los recolectores de registros o métricas crecen indefinidamente entre llamadas
- Los campos de
@dataclassusan valores predeterminados mutables incorrectamente (patrón relacionado)
Ejemplo Funcional
# broken_session_store.py
from dataclasses import dataclass, field
def broken_add_token(user_id: str, token: str, store: dict[str, list[str]] = {}) -> dict[str, list[str]]:
store.setdefault(user_id, []).append(token)
return store
# síntomas
broken_add_token("u1", "a")
broken_add_token("u2", "b")
print(broken_add_token("u1", "c")) # u1 ahora tiene tokens del almacén anterior contaminado
# fixed_session_store.py
def add_token(
user_id: str,
token: str,
store: dict[str, list[str]] | None = None,
) -> dict[str, list[str]]:
if store is None:
store = {}
store.setdefault(user_id, []).append(token)
return store
@dataclass
class SessionStore:
_data: dict[str, list[str]] = field(default_factory=dict)
def add(self, user_id: str, token: str) -> None:
self._data.setdefault(user_id, []).append(token)
# tests/test_session_store.py
from fixed_session_store import SessionStore, add_token
def test_add_token_isolated_calls() -> None:
assert add_token("u1", "x") == {"u1": ["x"]}
assert add_token("u2", "y") == {"u2": ["y"]}
def test_session_store_dataclass() -> None:
a = SessionStore()
b = SessionStore()
a.add("u1", "t1")
assert b._data == {}Lo que esto demuestra:
- El
{}predeterminado es un diccionario compartido para todas las llamadas abroken_add_token - El centinela
Nonecrea un diccionario nuevo por llamada cuando se omite @dataclassusafield(default_factory=dict)por la misma razón- Las pruebas demuestran el aislamiento entre instancias y llamadas
Análisis Profundo
Cómo Funciona
- Los valores predeterminados de las funciones se enlazan en la ejecución de
def, no por llamada. - El mismo objeto de lista/diccionario se reutiliza cuando el llamador omite el argumento.
- Los valores predeterminados de los campos de Dataclass siguen la misma regla sin
default_factory. - Los valores predeterminados inmutables (
None,0,"", tuplas) son seguros.
Valores Predeterminados Seguros vs. Inseguros
| Predeterminado | ¿Seguro? | Notas |
|---|---|---|
None | sí | usar centinela + mutable nuevo dentro |
Tupla () | sí | inmutable |
[], {}, set() | no | compartido entre llamadas |
datetime.now() | no | congelado en el momento de la definición |
Notas de Python
# la tipificación aclara la intención
def process(items: list[int] | None = None) -> list[int]:
working = list(items) if items is not None else []
working.append(1)
return workingTrampas Comunes
- Solo algunas solicitudes afectadas - un "heisenbug" bajo concurrencia reutiliza la memoria del proceso de trabajo. Solución: eliminar valores predeterminados mutables; reiniciar enmascara pero no soluciona.
- Campo de Dataclass
= {}- el mismo error en las clases. Solución:field(default_factory=dict). - Copiar manualmente el valor predeterminado es incorrecto -
bucket = bucket or []trata una lista vacía como si faltara. Solución:if bucket is None. - Mutar la lista del llamador cuando se proporciona - efecto secundario sorprendente cuando no se usa el valor predeterminado. Solución:
working = list(bucket)si no debes aliasar la entrada. - Caché con
@lru_cacheen métodos con estado mutable oculto - la caché + la mutación agravan la confusión. Solución: funciones puras o estado de instancia explícito. Dependsde FastAPI con valor predeterminado mutable - raro pero posible en firmas de dependencia personalizadas. Solución: usar el patrón de centinelaNone.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
Centinela None | funciones generales | necesitas un "vacío" distinguible como tercer estado |
dataclasses.field(default_factory=...) | clases | funciones simples |
| Nueva instancia de clase por solicitud | estado a nivel de solicitud de FastAPI | ayudantes simples y puros |
Preguntas Frecuentes
¿Por qué Python evalúa los valores predeterminados en el momento de la definición?
Los valores predeterminados se almacenan en el objeto de función (__defaults__, __kwdefaults__) para rendimiento e introspección. Los objetos mutables no deben usarse como esos valores almacenados.
¿Es seguro `def f(x=time.time())`?
No. La marca de tiempo se fija en el momento de la importación/definición. Pasa None y calcula time.time() dentro del cuerpo de la función.
¿Cómo encuentro estos errores en una base de código grande?
Busca = [], = {}, = set() en las líneas def y en los campos de dataclass sin default_factory. Las reglas de ruff 0.9+ marcan algunos patrones.
¿Cambia asyncio esto?
El mismo objeto de función se comparte entre tareas en un bucle de eventos. Los valores predeterminados mutables filtran el estado entre solicitudes concurrentes en un proceso.
¿Qué pasa con las listas predeterminadas en los modelos Pydantic?
Pydantic 2 usa default_factory internamente cuando usas Field(default_factory=list). items: list = [] simple en dataclasses sin procesar sigue siendo inseguro.
¿Puedo usar `bucket=[]` si nunca muto?
Si realmente nunca mutas, un valor predeterminado de tupla es más claro. Los valores predeterminados de lista vacía aún confunden a los lectores y a los linters.
¿Por qué no `bucket = bucket or []`?
Una lista vacía [] es "falsy"; los llamadores que pasan [] obtendrían una lista nueva inesperadamente. is None solo verifica el centinela.
¿Son mejores los globales a nivel de módulo?
No. Los globales crean los mismos problemas de estado compartido con peor testeabilidad. Prefiere parámetros explícitos o atributos de instancia.
¿Cómo depuro un presunto valor predeterminado compartido?
Imprime id(store) entre llamadas sin pasar store. IDs idénticos confirman el objeto de valor predeterminado compartido.
¿El estado `g` de Flask o el estado de solicitud de FastAPI reemplazan este patrón?
El estado a nivel de solicitud pertenece a los objetos g/solicitud, no a los valores predeterminados de la función. Los valores predeterminados son para todo el proceso, no para toda la solicitud.
¿Qué pasa con `functools.partial` con listas?
Partial enlaza los argumentos temprano; asegúrate de que los objetos mutables enlazados no se compartan involuntariamente entre partials.
¿Es `deepcopy` en el valor predeterminado una solución?
copy.deepcopy por llamada funciona pero es más lento y oscuro. El centinela None es idiomático y más rápido.
Relacionados
- Modelado de Dominio -
default_factoryde dataclass - Escenarios de Fugas de Memoria - crecimiento ilimitado de cachés compartidas
- Fragmentos de Refactorización de Depuración - antes/después de las correcciones
- Herramientas de Depuración - inspeccionar con pdb
- Mejores Prácticas de Arquitectura - evitar el estado mutable compartido
Versiones de la Pila: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.