En la lección anterior, nuestras funciones recibían datos de la forma más simple: un valor por posición para cada parámetro. Suficiente para empezar, pero corto para Papyrus: el cálculo del precio final depende de si el cliente es socio o no, y no queremos dos funciones casi idénticas (precio_normal() y precio_socio()) cuando la lógica es la misma con un matiz. Python ofrece un sistema de argumentos extraordinariamente flexible: argumentos por posición y por nombre, valores por defecto, cantidades variables con *args y **kwargs, y desempaquetado en la llamada. En esta lección lo recorremos entero, construimos la función estrella de la tienda — precio_final(base, socio=False, iva=IVA_LIBROS) — y saldamos la deuda que arrastramos desde el módulo 2: entender de verdad cómo funciona zip().

Contenido

  1. Argumentos posicionales y argumentos con nombre (keyword)
  2. Valores por defecto: precio_final() para socios y no socios
  3. La trampa de los valores por defecto mutables
  4. *args: cantidad variable de posicionales
  5. **kwargs: cantidad variable de argumentos con nombre
  6. El orden de los parámetros en la definición
  7. Desempaquetado con * y ** en la llamada
  8. Devolver varios valores (tuplas, presentación breve)
  9. Deuda saldada: zip() a fondo

Argumentos posicionales y argumentos con nombre

Hasta ahora hemos emparejado argumentos con parámetros por posición: el primero con el primero, el segundo con el segundo. Python también permite emparejarlos por nombre (argumentos keyword):

def etiquetar(titulo, precio):
    print(f"{titulo:<12} {precio:>6.2f} €")

etiquetar("Hamlet", 9.95)                 # posicionales
etiquetar(titulo="Hamlet", precio=9.95)   # con nombre
etiquetar(precio=9.95, titulo="Hamlet")   # con nombre: el orden da igual
etiquetar("Hamlet", precio=9.95)          # mezcla: posicionales primero
etiquetar(precio=9.95, "Hamlet")          # SyntaxError: posicional tras keyword

Reglas y criterio:

  • En una llamada mixta, los posicionales van siempre antes que los keyword.
  • Los argumentos con nombre hacen la llamada autoexplicativa: precio_final(12.50, True) obliga a mirar la definición para saber qué es ese True; precio_final(12.50, socio=True) se lee sola. Ya usaste esta idea en el módulo 2 con enumerate(catalogo, start=1) y en el módulo 1 con print(..., sep=", ", end=""): start, sep y end son argumentos keyword.
  • Regla práctica: por posición los datos obvios y obligatorios; por nombre las opciones y banderas.

Valores por defecto

Un parámetro puede declarar un valor que se usa si la llamada no lo proporciona. Es el mecanismo perfecto para la función de precios de Papyrus:

IVA_LIBROS = 0.04
DESCUENTO_SOCIO = 0.05

def precio_final(base, socio=False, iva=IVA_LIBROS):
    """Devuelve el precio final: descuento de socio (si procede) y luego IVA."""
    if socio:
        base = base * (1 - DESCUENTO_SOCIO)
    return round(base * (1 + iva), 2)

print(precio_final(12.50))              # 13.0   -> cliente normal
print(precio_final(12.50, socio=True))  # 12.35  -> Luis, socio
print(precio_final(12.50, socio=True, iva=0.21))  # 14.37 -> simulación de otro IVA

Desglose:

  • base no tiene valor por defecto: es obligatorio. Olvidarlo produce TypeError: precio_final() missing 1 required positional argument: 'base'.
  • socio=False y iva=IVA_LIBROS son opcionales: la llamada corta precio_final(12.50) cubre el caso más frecuente (cliente no socio, IVA de libros) y los casos especiales se piden explícitamente.
  • El orden de operaciones respeta la convención de la tienda desde el módulo 1: primero el descuento, después el IVA. Los precios de socio del catálogo salen clavados: 12.35, 9.83 y 15.71 € para La Odisea, Hamlet y El Quijote.

Un matiz que sorprende: los valores por defecto se evalúan una sola vez, cuando Python lee el def. iva=IVA_LIBROS toma el valor que IVA_LIBROS tenga en ese momento (0.04) y lo congela; si más tarde reasignaras la constante, el defecto no cambiaría. Con constantes e inmutables esto es inofensivo... pero conduce directamente a la trampa más famosa de Python.

La trampa de los valores por defecto mutables

Como el defecto se evalúa una única vez, usar un objeto mutable (una lista, por ejemplo) como valor por defecto significa que todas las llamadas comparten el mismo objeto:

def apuntar_pedido(titulo, pedidos=[]):     # ¡PELIGRO!
    pedidos.append(titulo)
    return pedidos

print(apuntar_pedido("Fausto"))     # ['Fausto']
print(apuntar_pedido("Hamlet"))     # ['Fausto', 'Hamlet']  <- ¡¿y ese Fausto?!

El segundo pedido "hereda" el primero porque la lista por defecto es la misma en ambas llamadas: se creó al leer el def y ahí sigue, acumulando. El idioma correcto usa None como centinela:

def apuntar_pedido(titulo, pedidos=None):
    """Añade el título a la lista de pedidos (o crea una lista nueva)."""
    if pedidos is None:
        pedidos = []                # lista NUEVA en cada llamada
    pedidos.append(titulo)
    return pedidos

print(apuntar_pedido("Fausto"))     # ['Fausto']
print(apuntar_pedido("Hamlet"))     # ['Hamlet']  <- correcto
Valor por defecto ¿Seguro? Ejemplos
Inmutable False, 0, 0.04, "es", None
Mutable No — se comparte entre llamadas [], {} (usa el patrón None)

*args: cantidad variable de posicionales

A veces no sabes cuántos valores llegarán. Un parámetro con * delante recoge todos los argumentos posicionales sobrantes:

def total_compra(*importes):
    """Devuelve la suma de todos los importes recibidos."""
    total = 0.0
    for importe in importes:
        total += importe
    return round(total, 2)

print(total_compra(12.35))               # 12.35 -> Luis se lleva un libro
print(total_compra(12.35, 9.83, 15.71))  # 37.89 -> se lleva tres
print(total_compra())                    # 0.0   -> hoy solo miraba
  • Dentro de la función, importes es una tupla con cero o más valores, y se recorre con for como cualquier secuencia.
  • El nombre args es pura convención (aquí importes es más expresivo); lo que importa es el *.
  • Ya conocías esto sin saberlo: print("a", "b", "c") acepta cualquier cantidad de argumentos gracias a este mecanismo.

**kwargs: cantidad variable de argumentos con nombre

El doble asterisco recoge los argumentos keyword sobrantes en un diccionario (pares nombre-valor; los diccionarios se estudian a fondo en el módulo 4, aquí solo necesitas la idea):

def ficha_libro(titulo, **datos):
    """Imprime una ficha con el título y cualquier dato extra."""
    print(f"Ficha de '{titulo}'")
    for campo, valor in datos.items():
        print(f"  {campo}: {valor}")

ficha_libro("Fausto", autor="Goethe", anio=1808, stock=0)
Ficha de 'Fausto'
  autor: Goethe
  anio: 1808
  stock: 0

Cada libro de Papyrus puede tener datos distintos (editorial, edición, idioma...) sin cambiar la firma de la función. datos.items() recorre los pares nombre-valor; de nuevo, el nombre kwargs (keyword arguments) es solo convención.

El orden de los parámetros en la definición

Cuando combines varias categorías, el orden en el def es fijo:

def ejemplo(obligatorio, con_defecto=1, *args, **kwargs):
    ...
Posición Categoría Ejemplo
1.º Posicionales obligatorios base
2.º Con valor por defecto socio=False, iva=IVA_LIBROS
3.º *args *importes
4.º **kwargs **datos

Saltarse el orden (def f(socio=False, base)) es SyntaxError: parameter without a default follows parameter with a default. En la práctica, la mayoría de tus funciones usarán solo las dos primeras categorías — *args/**kwargs son para casos genuinamente variables, no un comodín para no pensar la firma.

Desempaquetado con * y ** en la llamada

Los asteriscos también funcionan en sentido inverso: reparten una secuencia o un diccionario entre los parámetros de la llamada:

datos_fausto = ["Fausto", 21.00]
etiquetar(*datos_fausto)        # equivale a etiquetar("Fausto", 21.00)

opciones = {"socio": True, "iva": 0.04}
print(precio_final(21.00, **opciones))   # equivale a socio=True, iva=0.04 -> 20.75
  • *secuencia en una llamada desempaqueta sus elementos como argumentos posicionales.
  • **diccionario desempaqueta pares nombre-valor como argumentos keyword (las claves deben coincidir con nombres de parámetros).
  • Mnemotecnia: en el def, los asteriscos empaquetan (muchos argumentos → una variable); en la llamada, desempaquetan (una variable → muchos argumentos).

Devolver varios valores (presentación breve)

return puede entregar varios valores separados por comas; técnicamente devuelve una tupla (secuencia inmutable que veremos a fondo en el módulo 4), y lo habitual es desempaquetarla al recibirla — el mismo mecanismo de la asignación múltiple del módulo 1:

def resumen_precio(base, socio=False):
    """Devuelve (precio final, ahorro respecto al precio sin descuento)."""
    final = precio_final(base, socio=socio)
    sin_descuento = precio_final(base)
    return final, round(sin_descuento - final, 2)

precio, ahorro = resumen_precio(12.50, socio=True)
print(f"Luis paga {precio} € y ahorra {ahorro} €")   # Luis paga 12.35 € y ahorra 0.65 €

Con esto nos basta por ahora; las tuplas tendrán su propia lección (04-02).

Deuda saldada: zip() a fondo

En el módulo 2 usamos zip() de pasada; ha llegado el momento de entenderla. Papyrus guarda sus datos en tres listas paralelas — el elemento i de cada una describe el mismo libro:

catalogo = ["La Odisea", "Hamlet", "El Quijote"]
precios = [12.50, 9.95, 15.90]
stocks = [4, 6, 8]

zip() recibe varias secuencias y va produciendo tuplas con los elementos emparejados por posición: primero ("La Odisea", 12.50, 4), luego ("Hamlet", 9.95, 6), etc. Es como la cremallera (zipper) de una chaqueta: une un diente de cada lado en cada paso.

for titulo, precio, stock in zip(catalogo, precios, stocks):
    print(f"{titulo:<12} {precio:>6.2f} €  stock {stock}")

Compárala con el patrón de índices que usábamos hasta ahora:

Con índices Con zip()
Código for i, titulo in enumerate(catalogo): ... precios[i] ... stocks[i] for titulo, precio, stock in zip(...)
Legibilidad Regular: precios[i] obliga a rastrear i Alta: cada variable tiene nombre propio
¿Da el índice? No (combínala con enumerate si lo necesitas)
¿Listas de distinta longitud? IndexError si la que indexa es más larga Se detiene en la más corta, en silencio

Detalles importantes:

  • zip() es perezosa: no construye una lista de tuplas en memoria, las va entregando a medida que el for las pide. Si quieres verlas todas de golpe: list(zip(catalogo, precios))[('La Odisea', 12.5), ('Hamlet', 9.95), ('El Quijote', 15.9)]. Consecuencia práctica: un zip() ya recorrido queda agotado; crea uno nuevo para recorrerlo otra vez.
  • Que se detenga en la secuencia más corta es cómodo pero peligroso: si a catalogo le añades "Fausto" y olvidas añadir su precio, el for simplemente ignorará el libro sin avisar. Desde Python 3.10, zip(catalogo, precios, strict=True) lanza un error si las longitudes no coinciden — fíjate: strict es un argumento keyword, como los que acabas de aprender.
  • zip() también combina de maravilla con las funciones de esta lección:
def imprimir_tarifa_socios(titulos, bases):
    """Imprime cada título con su precio de socio."""
    for titulo, base in zip(titulos, bases):
        print(f"{titulo:<12} {precio_final(base, socio=True):>6.2f} €")

imprimir_tarifa_socios(catalogo, precios)
La Odisea     12.35 €
Hamlet         9.83 €
El Quijote    15.71 €

Estas listas paralelas empiezan a ser frágiles (tres listas que mantener sincronizadas a mano); en el módulo 4 conocerás estructuras que agrupan los datos de cada libro en un solo lugar. Hasta entonces, zip() es el pegamento oficial de Papyrus.

Errores Comunes y Consejos

  • Posicional después de keyword en la llamada (precio_final(socio=True, 12.50)): SyntaxError. Primero las posiciones, luego los nombres.
  • Repetir un argumento (precio_final(12.50, base=12.50)): TypeError: got multiple values for argument 'base'. El valor llegó por posición y por nombre a la vez.
  • [] o {} como valor por defecto: la trampa estrella. Usa None y crea el objeto dentro. Los linters (como los que sugiere VS Code) lo marcan; hazles caso.
  • Pasar banderas por posición (precio_final(12.50, True)): funciona, pero dentro de un mes nadie recordará qué era True. Escribe socio=True.
  • Confundir * en el def con * en la llamada: en la definición empaqueta; en la llamada desempaqueta. Si dudas, pregúntate de qué lado del paréntesis estás.
  • Abusar de *args/**kwargs: una firma explícita documenta y valida sola. Reserva los asteriscos para funciones genuinamente variádicas (como total_compra).
  • Olvidar que zip() calla con longitudes distintas: en datos que deben ir emparejados sí o sí, usa strict=True (Python ≥ 3.10).
  • Consejo: al diseñar una firma, ordena de más a menos importante y pon defectos a todo lo que tenga un valor "normal". La llamada típica debería ser la más corta.

Ejercicios

Ejercicio 1: el ticket variádico

Escribe ticket(cliente, *importes, socio=False) que imprima una línea Ticket de <cliente>, después cada importe pasado por precio_final(importe, socio=socio) (uno por línea, formato :>6.2f), y al final Total: X.XX € con la suma de los precios finales. Pruébala con ticket("Luis", 12.50, 9.95, 15.90, socio=True) — el total debe ser 37.89 € — y con ticket("Marta", 21.00).

Ejercicio 2: reponer sin sustos

Ana escribió esta función para apuntar reposiciones pendientes y algo va mal:

def apuntar_reposicion(titulo, pendientes=[]):
    pendientes.append(titulo)
    return pendientes

Ejecuta apuntar_reposicion("Fausto") y luego apuntar_reposicion("Hamlet"), observa el problema, explica por qué ocurre y corrígela con el patrón del centinela None.

Ejercicio 3: tarifa completa con zip()

Con catalogo, precios y stocks (más "Fausto", 21.00, 0 añadidos: usa catalogo + ["Fausto"], etc., para no modificar las originales), imprime con un solo bucle sobre zip() una tabla título | precio normal | precio socio | estado, donde el estado sea agotado si el stock es 0 y disponible en caso contrario. Usa precio_final() para ambos precios.

Soluciones

Ejercicio 1:

def ticket(cliente, *importes, socio=False):
    """Imprime el ticket de una compra con precios finales."""
    print(f"Ticket de {cliente}")
    total = 0.0
    for importe in importes:
        final = precio_final(importe, socio=socio)
        total += final
        print(f"{final:>6.2f} €")
    print(f"Total: {round(total, 2)} €")

ticket("Luis", 12.50, 9.95, 15.90, socio=True)   # 12.35 / 9.83 / 15.71, Total: 37.89 €
ticket("Marta", 21.00)                           # 21.84, Total: 21.84 €

Como socio va después de *importes, solo puede pasarse por nombre — Python jamás lo confundirá con un importe más. Es un truco de diseño muy usado.

Ejercicio 2: La segunda llamada devuelve ['Fausto', 'Hamlet']: ambas comparten la misma lista por defecto, creada una sola vez al leer el def. Corrección:

def apuntar_reposicion(titulo, pendientes=None):
    """Añade un título a la lista de reposiciones (nueva si no se pasa)."""
    if pendientes is None:
        pendientes = []
    pendientes.append(titulo)
    return pendientes

Ahora cada llamada sin lista recibe una lista recién creada, y quien tenga ya una lista propia puede pasarla: apuntar_reposicion("Hamlet", pendientes=mi_lista).

Ejercicio 3:

titulos = catalogo + ["Fausto"]
bases = precios + [21.00]
unidades = stocks + [0]

print(f"{'Título':<12} {'Normal':>8} {'Socio':>8}  Estado")
for titulo, base, stock in zip(titulos, bases, unidades):
    estado = "agotado" if stock == 0 else "disponible"
    print(f"{titulo:<12} {precio_final(base):>6.2f} € {precio_final(base, socio=True):>6.2f} €  {estado}")

Cada variable del for recibe su elemento emparejado por zip(); el ternario del módulo 2 resuelve el estado en una línea. Salida: Fausto aparece con 21.84 € / 20.75 € y agotado.

Conclusión

Las funciones de Papyrus ya hablan un lenguaje rico: argumentos posicionales para lo esencial, keyword para las opciones (socio=True), valores por defecto para el caso común — esquivando la trampa de los mutables —, *args/**kwargs para lo variable y desempaquetado con */** en las llamadas. Además, zip() ha dejado de ser magia: es la cremallera que recorre en paralelo las listas de la tienda, y precio_final(base, socio=False, iva=IVA_LIBROS) queda establecida como la función oficial de precios. En la próxima lección conoceremos una forma abreviada de escribir funciones diminutas y anónimas — las lambdas — y las pondremos a trabajar donde de verdad brillan: como criterio de sorted(), min() y max() para, por fin, ordenar el catálogo de Papyrus por precio.

Curso de Programación en Python

Módulo 1: Introducción a Python

Módulo 2: Estructuras de Control

Módulo 3: Funciones y Módulos

Módulo 4: Estructuras de Datos

Módulo 5: Programación Orientada a Objetos

Módulo 6: Manejo de Archivos

Módulo 7: Manejo de Errores y Excepciones

Módulo 8: Temas Avanzados

Módulo 9: Pruebas y Depuración

Módulo 10: Desarrollo Web con Python

Módulo 11: Ciencia de Datos con Python

Módulo 12: Proyecto Final

© Copyright 2026. Todos los derechos reservados