Hola! El momento de dejar de copiar y pegar
Si llegaste hasta aquí ya escribiste el mismo bloque de código dos o tres veces cambiando un número. A todas nos pasa 😅
Una función es trabajo con nombre. Lo escribes una vez, le pones un nombre, y lo usas todas las veces que quieras.
def con_igv(monto):
return round(monto * 1.18, 2)
print(con_igv(100))
print(con_igv(480.37))
118.0 566.84
Tres piezas y ya está: def para definirla, el nombre y los
paréntesis con lo que recibe, y return para devolver el resultado.
Y sí, la sangría manda igual que en el capítulo 4: lo que está dentro de la función lleva sus cuatro espacios.
return no es print
Esta confusión la tiene todo el mundo al principio, así que veámosla de frente.
def suma_mala(a, b):
print(a + b)
def suma_buena(a, b):
return a + b
resultado_malo = suma_mala(2, 3)
resultado_bueno = suma_buena(2, 3)
print('malo :', resultado_malo)
print('bueno:', resultado_bueno)
5 malo : None bueno: 5
Las dos "muestran" un 5, pero solo una devuelve algo con lo
que puedas seguir trabajando. La primera imprimió y se olvidó, así que
resultado_malo quedó en None.
La regla: una función calcula y devuelve; quien la llama decide si imprime. Si la función imprime por su cuenta, no la puedes reutilizar dentro de otro cálculo.
Argumentos: por posición y por nombre
def total_venta(unidades, precio, descuento):
bruto = unidades * precio
return round(bruto * (1 - descuento), 2)
print(total_venta(10, 45.0, 0.15))
print(total_venta(unidades=10, precio=45.0, descuento=0.15))
print(total_venta(10, descuento=0.15, precio=45.0))
382.5 382.5 382.5
Las tres llamadas hacen lo mismo. Pero mira la primera y dime de memoria qué significa ese 0,15 sin subir a leer la definición 🤔
Por eso, cuando una función recibe más de dos cosas, llámala por nombre. Tu yo del futuro te lo va a agradecer.
Y el orden importa cuando mezclas: los posicionales van primero, siempre.
total_venta(unidades=10, 45.0, 0.15)
SyntaxError: positional argument follows keyword argument
Valores por defecto
def total_venta(unidades, precio, descuento=0, igv=0.18):
bruto = unidades * precio * (1 - descuento)
return round(bruto * (1 + igv), 2)
print(total_venta(10, 45.0))
print(total_venta(10, 45.0, descuento=0.15))
print(total_venta(10, 45.0, igv=0))
531.0 451.35 450.0
Lo que casi siempre vale igual se pone por defecto, y quien necesite otra cosa lo dice. Así la llamada normal queda cortita y la excepcional sigue siendo posible.
La trampa del argumento por defecto
Esta te la enseño ahora porque es de las que muerden en producción y casi nadie la explica en español. Presta atención un minuto.
def agrega_venta(monto, historial=[]):
historial.append(monto)
return historial
print(agrega_venta(100))
print(agrega_venta(200))
print(agrega_venta(300))
[100] [100, 200] [100, 200, 300]
¿Ves lo que pasó? Cada llamada debería empezar con una lista vacía, y en vez de eso se van acumulando 😳
La razón: el valor por defecto se crea una sola vez, cuando se define la función, no en cada llamada. Así que las tres llamadas están usando la misma lista, exactamente como el aliasing del capítulo 3.
La solución es siempre la misma y se escribe así:
def agrega_venta(monto, historial=None):
if historial is None:
historial = []
historial.append(monto)
return historial
print(agrega_venta(100))
print(agrega_venta(200))
[100] [200]
Regla dura: nunca pongas una lista, un diccionario ni un conjunto como
valor por defecto. Usa None y créalo dentro. Con números,
textos y True o False no hay problema, porque esos no
se pueden modificar.
Devolver varias cosas
def resumen(montos):
return len(montos), sum(montos), sum(montos) / len(montos)
cuantas, total, promedio = resumen([480.37, 154.83, 2480.84])
print(cuantas)
print(f'{total:,.2f}')
print(f'{promedio:,.2f}')
3 3,116.04 1,038.68
Por dentro devuelve una tupla, como vimos en el capítulo 3, y al recibirla la desempaquetas en tres variables. Es limpísimo.
Cuando son más de tres cosas, mejor devuelve un diccionario: los nombres sobreviven y no dependes del orden.
def resumen(montos):
return {
'cuantas': len(montos),
'total': sum(montos),
'promedio': sum(montos) / len(montos),
'maximo': max(montos),
}
r = resumen([480.37, 154.83, 2480.84])
print(r['promedio'])
1038.68
Dónde vive cada variable
Lo que creas dentro de una función se muere cuando la función termina. Eso se llama alcance y es una protección, no un capricho.
def calcular():
interno = 42
return interno * 2
print(calcular())
print(interno)
NameError: name 'interno' is not defined
Al revés sí funciona: desde dentro se ve lo de fuera.
IGV = 0.18 # constante, en mayusculas por costumbre
def con_impuesto(monto):
return monto * (1 + IGV)
print(con_impuesto(100))
118.0
Ahora, que se pueda no significa que se deba. Una función que depende de variables de fuera es una función que solo funciona en tu cuaderno. Para constantes de verdad como el IGV está bien; para datos, pásalos como argumento.
Docstrings: explicar para qué sirve
Un texto entre triples comillas justo debajo del def es la
documentación de la función. No es adorno: help() la lee, tu editor
la muestra, y en un equipo es la diferencia entre que usen tu función o la
reescriban.
def clasifica_venta(monto):
"""
Devuelve la categoria comercial de una venta segun su monto.
Los cortes los fijo comercial en enero de 2026 y se revisan cada
semestre, asi que si cambian, se cambian aqui y en un solo sitio.
"""
if monto > 2000:
return 'Mayorista'
if monto > 500:
return 'Mediana'
return 'Pequeña'
print(clasifica_venta(780))
print(clasifica_venta.__doc__.strip().splitlines()[0])
Mediana Devuelve la categoria comercial de una venta segun su monto.
Fíjate en algo de esa función: como cada rama hace return, no
hacen falta los elif. Al hacer return se sale de la
función ahí mismo. Queda más plano y se lee mejor 🌸
lambda: funciones de una línea
Una lambda es una función chiquita y sin nombre. Se usa para
pasarla como argumento a otra cosa, y ahí es donde brilla.
ventas = [
{'ciudad': 'Lima', 'monto': 480.37},
{'ciudad': 'Cusco', 'monto': 2480.84},
{'ciudad': 'Piura', 'monto': 154.83},
]
por_monto = sorted(ventas, key=lambda v: v['monto'], reverse=True)
print([v['ciudad'] for v in por_monto])
['Cusco', 'Lima', 'Piura']
Ese key= le dice a sorted qué mirar de cada
elemento para comparar. Sin la lambda tendrías que definir una función entera
para algo de una línea.
Mi consejo: usa lambda solo para eso, para pasarla como argumento. Si le vas a poner nombre, entonces era una función normal.
Una función de verdad, con todo junto
def limpia_ciudad(texto, por_defecto='Sin ciudad'):
"""Normaliza el nombre de una ciudad que viene de un sistema externo."""
if texto is None:
return por_defecto
limpio = texto.strip().title()
if limpio == '':
return por_defecto
equivalencias = {'Lma': 'Lima', 'Aqp': 'Arequipa', 'Cuzco': 'Cusco'}
return equivalencias.get(limpio, limpio)
for entrada in [' lima ', 'CUZCO', 'Aqp', '', None, 'Piura']:
print(f'{repr(entrada):10} -> {limpia_ciudad(entrada)}')
' lima ' -> Lima 'CUZCO' -> Cusco 'Aqp' -> Arequipa '' -> Sin ciudad None -> Sin ciudad 'Piura' -> Piura
Eso es una función lista para producción: tiene su docstring, maneja el nulo, maneja el vacío, tiene un valor por defecto que quien llama puede cambiar, y las equivalencias están en un solo sitio donde se pueden corregir.
Y lo mejor: la puedes probar con seis entradas en dos segundos, que es exactamente lo que acabamos de hacer 💜
Ejercicios
1. Tu primera función
Escribe una función que reciba un monto y devuelva el descuento del 12%, redondeado a dos decimales.
def descuento(monto):
return round(monto * 0.12, 2)
print(descuento(480.37))
57.64
2. return contra print
Escribe la misma función con print en vez de
return e intenta usar su resultado en otra cuenta.
def descuento_print(monto):
print(round(monto * 0.12, 2))
r = descuento_print(480.37)
print(r)
57.64 None
El 57,64 lo viste, pero no lo tienes. Y None + algo revienta con
un TypeError, que es como se descubre este error en la práctica.
3. Con valor por defecto
Haz que el porcentaje de descuento sea un argumento con 12% por defecto, y pruébalo con y sin él.
def descuento(monto, porcentaje=0.12):
return round(monto * porcentaje, 2)
print(descuento(1000))
print(descuento(1000, porcentaje=0.25))
120.0 250.0
4. Tres valores de vuelta
Una función que reciba una lista de montos y devuelva el mínimo, el máximo y el rango.
def extremos(montos):
return min(montos), max(montos), max(montos) - min(montos)
bajo, alto, rango = extremos([480.37, 154.83, 2480.84])
print(bajo, alto, round(rango, 2))
154.83 2480.84 2326.01
5. La trampa, provocada
Escribe la función con la lista por defecto, llámala tres veces y explica la salida. Después arréglala.
def mal(venta, carrito=[]):
carrito.append(venta)
return carrito
print(mal('Abarrotes'), mal('Bebidas'), mal('Snacks'))
def bien(venta, carrito=None):
if carrito is None:
carrito = []
carrito.append(venta)
return carrito
print(bien('Abarrotes'), bien('Bebidas'), bien('Snacks'))
['Abarrotes', 'Bebidas', 'Snacks'] ['Abarrotes', 'Bebidas', 'Snacks'] ['Abarrotes', 'Bebidas', 'Snacks'] ['Abarrotes'] ['Bebidas'] ['Snacks']
En la primera línea salen tres veces la misma lista porque los tres
return devuelven el mismo objeto, ya con todo dentro.
6. Validar antes de calcular
Una función que calcule el ticket promedio y devuelva 0 si la lista viene vacía, en vez de reventar.
def ticket_promedio(montos):
if not montos:
return 0
return round(sum(montos) / len(montos), 2)
print(ticket_promedio([100, 200, 300]))
print(ticket_promedio([]))
200.0 0
Sin ese if te sale un ZeroDivisionError. Y las listas
vacías llegan más seguido de lo que uno cree: un filtro que no encontró nada ya
te dejó una.
7. El alcance
Comprueba que una variable creada dentro de una función no existe fuera.
def total_con_igv(monto):
igv = monto * 0.18 # solo existe aqui dentro
return monto + igv
print(total_con_igv(100))
print(igv)
NameError: name 'igv' is not defined
8. Ordenar con lambda
Ordena una lista de clientes por su nombre sin que las mayúsculas alteren el orden.
clientes = ['bodega rosa', 'Abarrotes Luz', 'MINIMARKET SOL'] print(sorted(clientes, key=lambda c: c.lower()))
['Abarrotes Luz', 'bodega rosa', 'MINIMARKET SOL']
9. Una función que usa otra
Escribe con_igv y con_descuento, y
después una tercera que las combine.
def con_igv(monto, igv=0.18):
return monto * (1 + igv)
def con_descuento(monto, dcto=0.10):
return monto * (1 - dcto)
def precio_final(base):
return round(con_igv(con_descuento(base)), 2)
print(precio_final(1000))
1062.0
Funciones chiquitas que se combinan es exactamente la idea. Y de paso: el orden importa, porque el IGV se calcula sobre el monto ya descontado y no al revés.
10. Con su docstring
Escribe una función que clasifique un puntaje de
satisfacción del 1 al 5, con docstring, y muéstrala con help.
def clasifica(puntaje):
"""Traduce un puntaje de 1 a 5 a una etiqueta de riesgo."""
if puntaje >= 4:
return 'Satisfecho'
if puntaje == 3:
return 'Neutral'
return 'En riesgo'
print(clasifica(5), clasifica(3), clasifica(1))
print(clasifica.__doc__)
Satisfecho Neutral En riesgo Traduce un puntaje de 1 a 5 a una etiqueta de riesgo.
Lo que te llevas
- 🎁 Una función calcula y
return; quien la llama decide si imprime. - 🏷️ Con más de dos argumentos, llámalos por nombre.
- 💣 Nunca una lista o un diccionario como valor por defecto: usa
None. - 📦 Varias cosas de vuelta se desempaquetan; si son muchas, devuelve un diccionario.
- 📝 El docstring dice para qué sirve y de dónde salieron los números.
En el capítulo 6 vamos a lo que pasa cuando algo falla: los errores más comunes con datos, cómo atraparlos sin taparlos, y cómo depurar sin volverte loca.
Que tengas un hermoso día! 🌟