# Funciones

URL canónica: https://missyera.com/guias/python-desde-cero/funciones/
Última actualización: 2026-11-14
Autora: Gera Flores (Miss Yera)

> **Respuesta corta**
>
> Una función empaqueta un trozo de trabajo bajo un nombre para no repetirlo. Se define con def, recibe argumentos y devuelve un resultado con return. Yo empiezo a escribir una en cuanto copio el mismo bloque por segunda vez. Y la trampa que más sorprende es poner una lista como valor por defecto: se crea una sola vez y queda compartida entre todas las llamadas.

Capítulo 6 de 17 del libro [Python desde cero](https://missyera.com/guias/python-desde-cero.md).

## 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))
```

```text
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)
```

```text
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.

![Árbol sobre qué debe hacer una función: si no devuelve nada, que modifique en el sitio. Si devuelve algo y no cambia lo que recibió, se puede probar y volver a usar. Si devuelve y además cambia lo que recibió, ahí se pierden las tardes.](https://missyera.com/static/diagramas/webp/py-05-funciones.webp)

*Una función que devuelve un resultado y además modifica lo que le diste parece que funciona, y es la que después te tiene comparando el antes con el después sin entender por qué salen iguales.*

## 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))
```

```text
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))
```

```text
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))
```

```text
[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))
```

```text
[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}')
```

```text
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'])
```

```text
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))
```

```text
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])
```

```text
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])
```

```text
['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)}')
```

```text
'  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))
```

```text
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'))
```

```text
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))
```

```text
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__)
```

```text
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')
```

```text
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)
```

```text
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))
```

```text
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)
```

```text
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))
```

```text
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))
```

```text
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'))
```

```text
['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([]))
```

```text
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()))
```

```text
['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))
```

```text
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__)
```

```text
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))
```

```text
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}')
```

```text
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](https://missyera.com/blog/skills-claude/) 🤖

En el capítulo [[cap:errores-y-depuración]] 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.

## Seguir leyendo

- Anterior: [Condicionales y bucles](https://missyera.com/guias/python-desde-cero/control-de-flujo.md)
- Siguiente: [Errores y depuración](https://missyera.com/guias/python-desde-cero/errores-y-depuracion.md)

## Contacto

- Diagnóstico de 30 minutos con Miss Yera: https://calendly.com/soymissyera/reunion-con-yera
- Correo: hola@missyera.com
- Esta página en HTML: https://missyera.com/guias/python-desde-cero/funciones/
- Índice de todas las páginas en texto: https://missyera.com/llms-index.txt
