Cuando Python evalúa una llamada a función, necesita mapear cada valor a un parámetro. Lo habitual es que ese mapeo sea uno a uno y completamente explícito. Pero hay situaciones donde no sabes cuántos argumentos recibirás, o necesitas reenviarlos a otra función sin inspeccionarlos. Para eso existen *args y **kwargs.
*args es empaquetado variádico posicional: el operador * en la firma le indica al intérprete que recoja todos los argumentos posicionales que sobren (los que no tienen parámetro explícito) y los empaquete en una tuple. **kwargs hace lo mismo pero para argumentos de palabra clave extra, y los empaqueta en un dict. Los nombres args y kwargs son convención; lo que importa es el operador (* / **).
El orden de los parámetros en la firma no es negociable: primero los parámetros posicionales normales, luego *args, después los parámetros keyword-only (los que sólo se pueden pasar por nombre porque vienen detrás del *), y al final **kwargs. Python impone este orden porque necesita saber cuándo termina la captura posicional para no ambigüar qué va a dónde.
El mismo operador que empaqueta también desempaqueta: en una llamada puedes escribir f(*mi_lista, **mi_dict) y Python expande esos iterables como si hubieras escrito cada elemento a mano. Esto es lo que hace que *args y **kwargs sean tan útiles en decoradores y wrappers: el wrapper captura todo con (*args, **kwargs) y reenvía todo con f(*args, **kwargs), sin necesidad de conocer la firma de f.
El riesgo real es usar esta herramienta para esconder una interfaz mal diseñada. Si una función siempre recibe exactamente tres parámetros con nombres concretos, declararlos explícitamente es siempre mejor: los errores de tipo y los IDEs te lo agradecen. *args / **kwargs brillan cuando la variabilidad es genuina, no cuando la usas para evitar pensar la interfaz.
import functools
import time
from collections.abc import Callable
from typing import Any
# ── Ejemplo 1: acumulador variádico ──────────────────────────────────────────
def total(*amounts: float, currency: str = "EUR") -> str:
"""
'amounts' llega como tuple[float, ...].
'currency' es keyword-only: solo se puede pasar por nombre.
"""
return f"{sum(amounts):.2f} {currency}"
# ── Ejemplo 2: constructor flexible que delega ───────────────────────────────
class StyledText:
def __init__(self, text: str, **styles: Any) -> None:
self.text = text
self.styles = styles # dict con lo que sea: bold=True, color="red", …
def render(self) -> str:
style_str = "; ".join(f"{k}={v}" for k, v in self.styles.items())
return f"<span style='{style_str}'>{self.text}</span>"
# ── Ejemplo 3: decorador de timing (patrón canónico de wrapper) ───────────────
def timed(func: Callable) -> Callable:
@functools.wraps(func) # preserva __name__, __doc__, etc.
def wrapper(*args: Any, **kwargs: Any) -> Any:
start = time.perf_counter()
result = func(*args, **kwargs) # reenvío completo sin inspectar
elapsed = time.perf_counter() - start
print(f"{func.__name__} tardó {elapsed:.4f}s")
return result
return wrapper
@timed
def fetch_data(url: str, *, timeout: int = 30, retries: int = 3) -> dict:
"""Simula una llamada HTTP con parámetros keyword-only."""
time.sleep(0.01)
return {"url": url, "timeout": timeout, "retries": retries}
# ── Ejemplo 4: desempaquetado en llamada ─────────────────────────────────────
def create_user(name: str, age: int, role: str) -> dict:
return {"name": name, "age": age, "role": role}
positional_args = ("Alice", 30)
keyword_args = {"role": "admin"}
# Equivale a create_user("Alice", 30, role="admin")
user = create_user(*positional_args, **keyword_args)
# ── Demo ─────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print(total(10, 20, 5.5)) # 35.50 EUR
print(total(10, 20, currency="USD")) # 30.00 USD
st = StyledText("Hola", bold=True, color="red", font_size=14)
print(st.render())
data = fetch_data("https://api.example.com/data", timeout=10)
print(data)
print(user)
Qué está pasando en cada decisión
total(*amounts, currency="EUR") muestra el orden de firma más común en la práctica: parámetro variádico primero, keyword-only con default después. Fíjate en que no hay forma de pasar currency posicionalmente; Python fuerza el nombre porque está detrás del *. Esto es exactamente el caso de uso correcto: la cantidad de sumandos es genuinamente variable, pero la moneda es un modificador semántico que merece nombre propio.
StyledText.__init__ captura estilos arbitrarios en **styles y los guarda como dict. Aquí la variabilidad es real: no tiene sentido enumerar todos los atributos CSS posibles como parámetros. El dict resultante es primero de los mejores argumentos para **kwargs: cuando el dominio del problema es open-ended por naturaleza.
El decorador timed es el patrón canónico. El wrapper no sabe ni le importa qué firma tiene func; su único trabajo es medir tiempo y reenviar todo intacto. func(*args, **kwargs) es la forma correcta de hacerlo porque preserva tanto los posicionales como los keyword. functools.wraps completa el cuadro: sin él, el wrapper ocultaría el nombre y el docstring de la función original, lo que rompe la introspección y herramientas como help().
El desempaquetado en llamada (create_user(*positional_args, **keyword_args)) es el espejo de la firma variádica. Cuando tienes datos en estructuras dinámicas (por ejemplo, una config leída de JSON), expandirlos directamente en la llamada es más limpio que construir la llamada a mano. Python mezcla ambos en el orden correcto siempre que no haya colisiones de nombres.
Errores que debes conocer
Error: pasar un argumento tanto posicionalmente como por nombre cuando hay desempaquetado, lo que produce TypeError: got multiple values for argument.
# ❌ Wrong
def greet(name: str, greeting: str) -> str:
return f"{greeting}, {name}!"
extra = {"name": "Bob"}
greet("Bob", **extra) # 'name' llega dos veces
# ✅ Right
extra = {"greeting": "Hola"}
greet("Bob", **extra) # posicional + keyword sin solapamiento
Antes de desempaquetar un dict en una llamada, asegúrate de que sus claves no colisionan con los posicionales que ya pasaste.
Error: usar **kwargs en lugar de parámetros explícitos y perder la validación en tiempo de llamada.
# ❌ Wrong
def connect(**kwargs):
host = kwargs["host"] # KeyError en runtime si falta, no en la llamada
port = kwargs["port"]
...
# ✅ Right
def connect(host: str, port: int, *, timeout: float = 5.0) -> None:
...
Con la firma explícita, Python lanza TypeError en el momento de la llamada si falta un argumento, los type checkers pueden inferir tipos, y el IDE puede autocompletar. **kwargs aquí sólo aplaza el error y lo hace más difícil de rastrear.
Error: olvidar que *args dentro de la función es una tuple, no una lista, e intentar mutarla.
# ❌ Wrong
def append_default(*args):
args.append(0) # AttributeError: 'tuple' object has no attribute 'append'
# ✅ Right
def append_default(*args):
items = list(args)
items.append(0)
return items
Si necesitas modificar los argumentos capturados, conviértelos explícitamente a list primero; la inmutabilidad de tuple es intencional y protege contra modificaciones accidentales.
N° 56