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 😅
Y hazte esta pregunta cada vez que copies algo: ¿este bloque lo estoy pegando por segunda vez? Si la respuesta es sí, ahí va una función. No conozco regla mejor que esa 💛
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 5: 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 4.
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 4, 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 💜
Cuando no sabes cuántos argumentos van a venir
Hay funciones a las que quieres pasarle tres cosas hoy y siete mañana. Para eso están el asterisco y el doble asterisco.
def resume(*montos, moneda='S/', decimales=2): total = sum(montos) return f'{len(montos)} ventas, total {moneda}{total:.{decimales}f}' print(resume(120.5, 340, 89.9)) print(resume(120.5, 340, moneda='US$', decimales=1))
3 ventas, total S/550.40 2 ventas, total US$460.5
El *montos recoge en una tupla todo lo que llegue por posición.
Y lo que va después del asterisco, moneda y decimales,
solo se puede pasar por nombre 🏷️
Eso último no es un capricho de Python: es lo que evita que alguien escriba
resume(120.5, 340, 'US$') y que ese 'US$' se cuele en
la suma.
El doble asterisco hace lo mismo con los argumentos con nombre, y los recoge en un diccionario:
def registra(**datos): return ', '.join(f'{k}={v}' for k, v in sorted(datos.items())) print(registra(cliente='Rosita', monto=340, canal='WhatsApp'))
canal=WhatsApp, cliente=Rosita, monto=340
Y lo mejor es que funciona al revés. Si ya tienes una lista o un diccionario, el asterisco los desempaqueta en la llamada:
ventas = [120.5, 340, 89.9] print('desempaquetando una lista:', resume(*ventas)) config = {'moneda': 'US$', 'decimales': 0} print('y un diccionario:', resume(*ventas, **config))
desempaquetando una lista: 3 ventas, total S/550.40 y un diccionario: 3 ventas, total US$550
Esto es lo que hace que puedas guardar la configuración en un diccionario, o leerla de un archivo, y pasársela a la función sin escribir cada parámetro a mano 📦
Lo vas a ver muchísimo en las librerías. Cuando la documentación de pandas
dice **kwargs, te está diciendo "pásame lo que quieras, que yo se lo
paso a quien corresponda" 🐼
Anotaciones: decir qué entra y qué sale
El docstring explica para qué sirve la función. Las anotaciones dicen de qué tipo es cada cosa, y se escriben en la misma línea de la definición 🏷️
def total_con_igv(monto: float, tasa: float = 0.18) -> float: 'Devuelve el monto con el IGV incluido, redondeado a dos decimales.' return round(monto * (1 + tasa), 2) print(total_con_igv(480.37)) print(total_con_igv.__annotations__)
566.84
{'monto': <class 'float'>, 'tasa': <class 'float'>, 'return': <class 'float'>}
Los dos puntos después de cada parámetro dicen qué tipo espera, y la flecha
del final dice qué devuelve. Se quedan guardadas en
__annotations__, así que no son un comentario: son datos.
Para qué sirven de verdad 🎯
- 👀 Para quien lee. Con la anotación no tienes que bajar a leer el cuerpo para saber si esa función devuelve un número o un texto.
- 💡 Para tu editor. VS Code y PyCharm las leen y te avisan antes de ejecutar nada.
- 📄 Para las dataclass del capítulo 9, que las usan de verdad para armar la clase.
Y ahora lo importante, que es lo que casi nunca se dice:
def etiqueta(codigo: str) -> int: return 'cliente ' + codigo print('promete un int y devuelve texto:', repr(etiqueta('C0045'))) print('y Python no dijo absolutamente nada')
promete un int y devuelve texto: 'cliente C0045' y Python no dijo absolutamente nada
Python no comprueba las anotaciones. Son documentación que se puede leer con código, y nada más 🤷♀️
Esa función promete devolver un entero, devuelve un texto, y corre tan
contenta. Si quieres que alguien las verifique de verdad hace falta una
herramienta aparte, tipo mypy, que las lee y te avisa sin ejecutar el
programa.
try: total_con_igv('480.37') except TypeError as e: print('TypeError:', e)
TypeError: can't multiply sequence by non-int of type 'float'
Fíjate en dónde falla: en la multiplicación, no en la puerta 🚪
La anotación decía float y le pasé un texto, y a Python le dio
igual hasta que llegó el momento de multiplicar. Por eso el mensaje habla de
secuencias y no de tu parámetro.
Mi costumbre: anotar las funciones que van a usar otras personas, y no molestarme en los scripts de exploración. Es como el docstring, la pongo cuando la función deja de ser mía y pasa a ser de todos 🌸
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.
11. Una función que recibe otra función
En Python las funciones son valores, así que se pueden guardar en variables y pasar como argumento.
def aplica_a_todas(ventas, calculo): return [calculo(v) for v in ventas] def con_igv(monto): return round(monto * 1.18, 2) def redondea_a_soles(monto): return round(monto) ventas = [120.5, 340.0, 89.9] print('con IGV :', aplica_a_todas(ventas, con_igv)) print('redondeado:', aplica_a_todas(ventas, redondea_a_soles)) print('y con lambda:', aplica_a_todas(ventas, lambda m: m / 2))
con IGV : [142.19, 401.2, 106.08] redondeado: [120, 340, 90] y con lambda: [60.25, 170.0, 44.95]
Fíjate en que con_igv va sin paréntesis al
pasarla. Con paréntesis la llamarías; sin ellos la entregas 🎁
Ese es exactamente el error del ejercicio 3 de este capítulo visto desde el
otro lado, y es el mismo patrón que usas cada vez que escribes
sorted(lista, key=...).
12. La función que se llama a sí misma
Recorrer un diccionario con diccionarios dentro, sin saber cuántos niveles tiene.
pedido = {
'cliente': 'Rosita',
'entrega': {'ciudad': 'Lima', 'distrito': 'Villa El Salvador'},
'items': {'abarrotes': {'arroz': 12, 'aceite': 4}, 'bebidas': {'gaseosa': 20}},
}
def aplana(d, prefijo=''):
salida = {}
for clave, valor in d.items():
nombre = f'{prefijo}{clave}'
if isinstance(valor, dict):
salida.update(aplana(valor, prefijo=f'{nombre}.'))
else:
salida[nombre] = valor
return salida
for clave, valor in aplana(pedido).items():
print(f'{clave:28s} {valor}')
cliente Rosita entrega.ciudad Lima entrega.distrito Villa El Salvador items.abarrotes.arroz 12 items.abarrotes.aceite 4 items.bebidas.gaseosa 20
Una función que se llama a sí misma se llama recursiva, y da más miedo del que merece 👻
Lo único que hay que asegurar es que en algún momento pare. Aquí para sola: cuando el valor deja de ser un diccionario, no vuelve a llamarse.
Y este ejercicio concreto lo vas a necesitar de verdad. Los JSON que devuelven las APIs vienen anidados así, y para meterlos en una tabla hay que aplanarlos primero 🗜️
La función que limpia y ensucia a la vez
Falta una sola cosa antes de cerrar funciones, y es la que más veces he visto acabar en una tarde entera de buscar dónde cambió el dato 🔎
La trampa
Escribes una función que limpia una lista de ciudades y devuelve el resultado. La usas, guardas lo que devuelve y sigues.
def limpiar(ciudades): for i, c in enumerate(ciudades): ciudades[i] = c.strip().lower() return ciudades limpias = limpiar(originales)
Qué está mal
La función modificó originales también 🪞 En Python los argumentos se pasan por referencia, así que dentro de la función ciudades y fuera originales son la misma lista. limpias no es una copia limpia: es otro nombre para lo mismo.
Y como además devuelve la lista, todo aparenta que devolvió algo nuevo. Dos líneas más abajo comparas el antes con el después y salen idénticos, y te vuelves loca buscando por qué.
Una función que devuelve algo no debería modificar lo que recibe. O modifica en el sitio y no devuelve nada, o construye una lista nueva y la devuelve. Las dos cosas a la vez es la receta de las tardes perdidas.
Comprueba que lo tienes
Escribes def guarda(dato, lista=[]) y al llamarla dos veces la lista trae lo de la llamada anterior. ¿Por qué?
- Porque el valor por defecto se crea una sola vez, al definir
- Porque falta un return
- Porque las listas son globales
- Porque hay que llamarla con lista=[] cada vez
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.
Y si de todo el capítulo te llevas una sola frase, que sea esta:
Una función o devuelve algo o cambia algo. Las dos a la vez es la receta de las tardes perdidas.
Y esto de empaquetar lo que repites para no volver a escribirlo no es solo de Python: es lo mismo que hago con la IA para no repetir la misma instrucción cada semana, que lo conté en skills de Claude 🤖
En el capítulo 7 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! 🌟
Preguntas frecuentes
¿Cómo se hace una función en Python?
Con def, el nombre, los paréntesis con lo que recibe y dos puntos. Lo que va dentro se escribe indentado, y return devuelve el resultado.
¿Qué hace return en Python?
Devuelve un valor y termina la función ahí mismo. Sin return la función devuelve None, que es el motivo número uno de que algo salga vacío sin dar error.
¿Qué es una función lambda?
Una función de una sola línea y sin nombre. Sirve para pasarla como argumento a otra cosa, y si la necesitas dos veces, mejor escríbela con def.
¿Cuál es la diferencia entre parámetro y argumento?
El parámetro es el nombre que la función usa por dentro; el argumento es el valor que le pasas al llamarla.
Practica este capítulo 📓
Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab de un clic y no hay que instalar nada. Donde veas %%revisa, escribe tu respuesta y el cuaderno te dice si te salió.
¿Prefieres trabajar en tu máquina? Bájate el cuaderno de práctica o el de soluciones. Todos están también en github.com/soymissyera/MissYeraEjercicios.