functools.partial toma una función existente y algunos de sus argumentos —posicionales, por clave, o ambos— y devuelve un nuevo callable que recuerda esos argumentos para siempre. No es una envoltura que evalúa nada todavía; es más parecido a rellenar parte de un formulario y guardarlo para completarlo más tarde.
El mecanismo interno es sencillo: partial crea un objeto de tipo functools.partial que almacena la función original en .func, los argumentos posicionales fijos en .args y los de clave en .keywords. Cuando lo llamas, Python fusiona lo que guardaste con lo que pasas en ese momento y delega a la función original. No hay magia: es una composición de llamada diferida.
¿Cuándo usarlo? Cada vez que tienes una función con N parámetros y necesitas adaptarla a una interfaz que espera N−k parámetros —callbacks de eventos, funciones de orden superior como map o sorted, sistemas de plugins que llaman a tus hooks con una firma fija. Si te encuentras escribiendo lambda x: pow(2, x) solo para fijar el primer argumento de pow, partial es la respuesta más honesta.
Lo que rompe cuando lo ignoras: el lambda equivalente funciona, pero pierde contexto. partial(pow, 2).__name__ no existe como tal, pero partial(pow, 2).func.__name__ es 'pow' y repr() muestra claramente functools.partial(<built-in function pow>, 2). Con un lambda opaco en un traceback o en un log, solo ves <lambda>. Además, si fijas un argumento en el lugar equivocado —posicional cuando debería ser por clave— obtendrás un TypeError en el momento de la llamada, no al crear el partial, así que el error aparece lejos de donde está el bug.
import functools
import logging
from typing import Callable
# ── 1. Caso canónico: fijar el primer argumento de pow ──────────────────
power_of_two = functools.partial(pow, 2) # 2 ** x
power_of_ten = functools.partial(pow, 10) # 10 ** x
print(power_of_two(8)) # 256
print(power_of_ten(3)) # 1000
# ── 2. Fijar argumentos por clave: configurar una función genérica ──────
def send_message(text: str, *, level: str = "info", prefix: str = "") -> str:
return f"[{level.upper()}] {prefix}{text}"
# Versión preconfigurada para errores críticos con prefijo de sistema
critical = functools.partial(send_message, level="critical", prefix="SYS >> ")
print(critical("Disk full")) # [CRITICAL] SYS >> Disk full
print(critical("Connection lost")) # [CRITICAL] SYS >> Connection lost
# ── 3. Adaptar firma para sorted/map/filter ─────────────────────────────
data = ["banana", "Apple", "cherry", "avocado"]
# sorted espera key=callable(item) → un argumento
# str.startswith espera (self, prefix) → necesitamos fijar prefix
starts_with_a = functools.partial(str.startswith, "a")
# ^unbound method ^prefix fijo
# Ojo: str.startswith como método sin instancia recibe (self, prefix)
# partial fija prefix="a", así que al llamar con la string queda bien
filtered = [w for w in data if w.lower().startswith("a")]
# Uso más natural: fijar el argumento como kwarg
has_prefix = functools.partial(lambda s, p: s.lower().startswith(p), p="a")
print(list(filter(has_prefix, data))) # ['Apple', 'avocado']
# ── 4. Callbacks con contexto: patrón real en GUIs y sistemas de eventos ─
def on_button_click(button_id: int, event: dict) -> None:
print(f"Button {button_id} clicked with event {event}")
# El sistema de eventos llama a callback(event) → solo un argumento
# Necesitamos inyectar button_id sin clausuras ad-hoc por cada botón
def register_buttons(ids: list[int]) -> dict[int, Callable]:
return {
btn_id: functools.partial(on_button_click, btn_id)
for btn_id in ids
}
handlers = register_buttons([1, 2, 3])
handlers[2]({"x": 10, "y": 20}) # Button 2 clicked with event {'x': 10, 'y': 20}
# ── 5. Introspección: lo que partial conserva ──────────────────────────
p = functools.partial(send_message, level="warning")
print(p.func) # <function send_message at 0x...>
print(p.args) # ()
print(p.keywords) # {'level': 'warning'}
print(repr(p)) # functools.partial(<function send_message ...>, level='warning')
Qué está pasando en cada decisión
El ejemplo 1 es el más didáctico: partial(pow, 2) fija el primer posicional, así que cuando llamas power_of_two(8) Python ejecuta pow(2, 8). El orden importa igual que en la llamada original —no hay reordenación automática.
El ejemplo 2 muestra por qué partial con kwargs es mejor que una función envoltura cuando el propósito es solo preconfigurar. Podrías escribir def critical(text): return send_message(text, level="critical", prefix="SYS >> "), pero con partial el origen es explícito: critical.func y critical.keywords documentan solos de dónde viene y qué fijó.
El ejemplo 3 merece atención: str.startswith como atributo de clase (no de instancia) es un método no enlazado que recibe (self, prefix[, start[, end]]). Si intentas partial(str.startswith, "a") estarías fijando self="a", no prefix. Por eso el ejemplo usa una lambda intermedia con kwarg explícito —es el caso donde la solución más directa no es partial puro, sino una combinación. Reconocer ese límite es parte de usarlo bien.
El ejemplo 4 es el patrón de producción más frecuente: sistemas de eventos, frameworks de UI, y schedulers esperan callables con una firma fija. partial es más limpio que crear una clausura nueva en cada iteración porque el objeto resultante es inspeccionable —puedes leer handler.args[0] para saber qué button_id tiene registrado, algo imposible con un lambda.
El ejemplo 5 muestra la introspección directamente. Cuando algo falla en producción y tienes un callable en un traceback, poder llamar .func y .keywords sobre él marca la diferencia entre un debug de dos minutos y uno de veinte.
Errores que debes conocer
Error: fijar un argumento posicional cuando la función espera ese slot ocupado por otro valor en la llamada posterior.
# ❌ Wrong
import functools
# Quiero fijar el exponente (segundo arg), no la base
square = functools.partial(pow, 2) # Esto fija la BASE en 2, no el exponente
print(square(3)) # pow(2, 3) = 8, pero queríamos pow(3, 2) = 9
# ✅ Right
# pow no tiene keyword para el exponente, así que usamos una lambda
# o reordenamos con una función intermedia
def pow_to(exp: int, base: int) -> int:
return pow(base, exp)
square = functools.partial(pow_to, 2) # fija exp=2
print(square(3)) # pow_to(2, 3) → pow(3, 2) = 9
print(square(5)) # 25
partial no puede reordenar argumentos posicionales: solo prepende los que fijaste. Si necesitas fijar un argumento que no está al principio y la función no acepta kwargs, la solución es un adaptador con la firma reordenada.
Error: confundir el momento de evaluación —creer que partial evalúa los argumentos fijos de forma lazy.
# ❌ Wrong: asumir que config se re-evalúa en cada llamada
config = {"retries": 3}
import functools
def connect(host: str, options: dict) -> str:
return f"connect({host}, retries={options['retries']})"
connector = functools.partial(connect, options=config)
config["retries"] = 10 # Mutamos el dict después de crear el partial
# El partial guarda la REFERENCIA al dict, no una copia
print(connector("db.local")) # connect(db.local, retries=10) — ¡mutado!
# ✅ Right: si necesitas un snapshot, pasa una copia en el momento de crear el partial
connector_frozen = functools.partial(connect, options=dict(config))
config["retries"] = 99
print(connector_frozen("db.local")) # connect(db.local, retries=10) — estable
partial guarda referencias, no valores copiados. Si pasas un objeto mutable, cualquier mutación posterior se refleja en todas las llamadas futuras —exactamente como ocurre con los argumentos por defecto mutables en definiciones de funciones.
N° 114