Al cerrar el módulo 2 dejamos un diagnóstico claro: el código de Papyrus funciona, pero vive suelto. La búsqueda de libros está dentro de menu.py, el cálculo del precio de socio se repite en venta_socio.py y en ticket.py, y cada mejora obliga a Ana a tocar varios scripts. La solución prometida son las funciones: bloques de código con nombre propio que defines una vez y ejecutas cuantas veces quieras. Y aquí va la buena noticia: llevas usándolas desde el primer día — print(), len(), input(), enumerate() son funciones que ya venían hechas. En esta lección aprenderás a crear las tuyas con def, a devolver resultados con return, a documentarlas con docstrings y a entender dónde "viven" sus variables. Como proyecto, empezaremos la reorganización de menu.py extrayendo sus dos primeras piezas: mostrar_catalogo() y buscar_libro().

Contenido

  1. Qué es una función y por qué usarla
  2. Definir con def y llamar a la función
  3. Parámetros: datos de entrada
  4. return: devolver resultados (y el caso None)
  5. Docstrings: documentar la función
  6. Ámbito de las variables: local frente a global
  7. Proyecto: primera refactorización de menu.py

Qué es una función y por qué usarla

Una función es un bloque de código con nombre, que opcionalmente recibe datos de entrada (parámetros) y opcionalmente devuelve un resultado. Piensa en la fotocopiadora de Papyrus: Ana no reconstruye la máquina cada vez que necesita una copia; mete un original (entrada), pulsa un botón (llamada) y recoge la copia (resultado).

Las tres razones clásicas para usar funciones:

Motivo Sin funciones Con funciones
Reutilización El cálculo del precio de socio está copiado en 3 scripts Se escribe una vez y se llama desde donde haga falta
Mantenimiento Cambiar el IVA obliga a editar cada copia (y olvidar una = bug) Se cambia en un único sitio
Legibilidad menu.py es un bloque de 35 líneas que hay que leer entero mostrar_catalogo() se explica sola por su nombre

Definir con def y llamar a la función

La sintaxis mínima:

def saludar():
    print("Bienvenida a la gestión de Papyrus")

saludar()
saludar()

Desglose línea a línea:

  • def (de define) inicia la definición.
  • saludar es el nombre: mismas reglas que las variables (snake_case, PEP 8). Suele ser un verbo, porque una función hace algo.
  • Los paréntesis () contendrán los parámetros (aquí, ninguno) y los dos puntos : abren el bloque.
  • El cuerpo va indentado, exactamente igual que en if o for.
  • saludar() — con paréntesis — llama (ejecuta) la función. Sin paréntesis, saludar solo nombra la función sin ejecutarla; es un error silencioso muy común.

Un detalle importante: definir no es ejecutar. Python lee el def, memoriza el bloque bajo ese nombre y sigue adelante. El cuerpo solo corre cuando alguien llama a la función. Por eso este script imprime en un orden que sorprende a los principiantes:

def despedir():
    print("2. Dentro de la función")

print("1. Antes de llamar")
despedir()
print("3. Después de llamar")
1. Antes de llamar
2. Dentro de la función
3. Después de llamar
sequenceDiagram
    participant S as Script principal
    participant F as despedir()
    S->>S: print("1. Antes de llamar")
    S->>F: llamada despedir()
    F->>F: print("2. Dentro de la función")
    F-->>S: fin de la función (vuelve al punto de llamada)
    S->>S: print("3. Después de llamar")

Parámetros: datos de entrada

Una función sin entradas siempre hace lo mismo. Los parámetros la hacen flexible:

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

etiquetar("Hamlet", 9.95)
etiquetar("Fausto", 21.00)
  • titulo y precio son parámetros: nombres que la función usa internamente.
  • "Hamlet" y 9.95 son argumentos: los valores concretos que envías en la llamada. El primero va al primer parámetro, el segundo al segundo (por posición).
  • En cada llamada, los parámetros nacen con los valores recibidos, la función trabaja y al terminar desaparecen.

De momento nos basta con esta forma básica; la lección 03-02 está dedicada por completo a los tipos de argumentos (con nombre, valores por defecto, cantidades variables...).

return: devolver resultados (y el caso None)

print() muestra, pero no entrega. Si quieres usar el resultado de una función en un cálculo posterior, necesitas return:

def precio_socio(base):
    """Precio para socios: descuento del 5% y luego IVA del 4%."""
    final = base * (1 - 0.05) * (1 + 0.04)
    return round(final, 2)

precio_hamlet = precio_socio(9.95)
print(f"Luis paga {precio_hamlet} €")          # Luis paga 9.83 €
print(f"Dos ejemplares: {precio_socio(9.95) * 2} €")

Claves de return:

  • Entrega un valor al punto de llamada: la expresión precio_socio(9.95) vale 9.83, y puedes asignarla, imprimirla u operarla.
  • Termina la función inmediatamente: el código posterior al return ejecutado no corre nunca. Esto lo convierte en la salida de emergencia perfecta — recuerda el consejo del módulo 2: donde break solo sale del bucle más interno, return sale de la función entera, bucles anidados incluidos.
  • Una función puede tener varios return (por ejemplo, uno dentro de un if y otro al final); en cada llamada se ejecuta como mucho uno.

¿Y si una función no tiene return, o escribe return a secas? Devuelve None, el "valor vacío" que conociste en el módulo 1:

def avisar_agotado(titulo):
    print(f"'{titulo}' está agotado.")

resultado = avisar_agotado("Fausto")
print(resultado)        # None

La tabla que evita el 90 % de las confusiones iniciales:

print(x) dentro de la función return x
¿Muestra algo en pantalla? No (salvo que quien llama lo imprima)
¿La llamada tiene un valor útil? No (vale None) Sí (vale x)
¿Se puede seguir operando con el resultado? No
Uso típico Informar al usuario Calcular para otro código

Regla práctica para Papyrus: las funciones que calculan (precios, búsquedas) deben devolver con return; las que presentan (mostrar el catálogo) pueden limitarse a print() y devolver None.

Docstrings: documentar la función

La primera línea del cuerpo, si es una cadena, se convierte en la docstring: la documentación oficial de la función. Se escribe entre triples comillas:

def precio_socio(base):
    """Devuelve el precio final para socios de Papyrus.

    Aplica el descuento de socio (5%) y después el IVA de libros (4%),
    redondeando a 2 decimales.
    """
    return round(base * (1 - 0.05) * (1 + 0.04), 2)

help(precio_socio)      # muestra la docstring en el REPL
  • No es un comentario decorativo: help(), VS Code (al pasar el ratón sobre la llamada) y las herramientas de documentación la leen.
  • Convención: primera línea en forma de resumen que quepa en una línea; si hace falta más detalle, línea en blanco y párrafo explicativo.
  • Los comentarios # explican cómo funciona un fragmento por dentro; la docstring explica qué hace la función y cómo usarla desde fuera.

Ámbito de las variables: local frente a global

¿Dónde "vive" una variable? Depende de dónde se asigne:

NOMBRE_TIENDA = "Papyrus"          # ámbito global (nivel del script)

def crear_ticket():
    linea = f"Ticket de {NOMBRE_TIENDA}"   # linea: ámbito LOCAL
    return linea

print(crear_ticket())   # Ticket de Papyrus
print(linea)            # NameError: name 'linea' is not defined
  • linea es local: nace al llamar a la función y muere al terminar. Fuera, no existe. Esto es una ventaja enorme: dos funciones pueden usar linea, i o total sin pisarse.
  • NOMBRE_TIENDA es global: se asignó en el nivel del script, y las funciones pueden leerla sin hacer nada especial. Python, al no encontrar NOMBRE_TIENDA en el ámbito local, la busca "un piso más arriba".

El matiz delicado llega al intentar modificar una global dentro de una función:

caja = 0.0

def cobrar(importe):
    caja = caja + importe   # UnboundLocalError

Al ver la asignación caja = ..., Python decide que caja es local a cobrar... y entonces caja + importe intenta leer una local que aún no tiene valor. Existe una palabra clave que lo "arregla":

def cobrar(importe):
    global caja             # funciona, pero es mala idea
    caja = caja + importe

global caja le dice a Python "cuando asigne caja, usa la global". Funciona, pero conviene evitarla: una función que modifica globales tiene efectos invisibles desde la llamada — leyendo cobrar(12.35) nadie sospecha que caja cambió, y con varias funciones tocando las mismas globales el programa se vuelve indepurable. La alternativa limpia es recibir y devolver:

def cobrar(caja, importe):
    """Devuelve el nuevo total de caja tras cobrar el importe."""
    return caja + importe

caja = 0.0
caja = cobrar(caja, 12.35)   # el cambio se VE en el punto de llamada

Resumen de reglas de ámbito:

Operación dentro de la función ¿Necesita algo especial? ¿Recomendable?
Leer una global (constantes como IVA_LIBROS) No Sí — es el uso natural de las constantes
Asignar una variable nueva No (será local)
Modificar una global Sí, global nombre No — recibe el dato como parámetro y devuelve el resultado

Proyecto: primera refactorización de menu.py

Cumplamos la promesa del módulo 2. Refactorizar es reorganizar código sin cambiar lo que hace, y menu.py lo pide a gritos: extraeremos la opción 1 a mostrar_catalogo() y el corazón de la opción 2 a buscar_libro(). En la carpeta papyrus (con el .venv activado), la nueva versión:

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


def mostrar_catalogo():
    """Imprime la tabla de títulos, precios y stock."""
    print(f"\n{'Título':<12} {'Precio':>8} {'Stock':>6}")
    print("-" * 28)
    for i, titulo in enumerate(catalogo):
        print(f"{titulo:<12} {precios[i]:>6.2f} € {stocks[i]:>6}")


def buscar_libro(buscado):
    """Devuelve el índice del libro cuyo título coincide, o None si no está."""
    for i, titulo in enumerate(catalogo):
        if titulo.lower() == buscado.lower():
            return i
    return None


print(f"Bienvenida a la gestión de {NOMBRE_TIENDA}")

while True:
    print()
    print("1. Ver catálogo")
    print("2. Buscar un libro")
    print("3. Salir")
    opcion = input("Elige una opción (1-3): ")

    match opcion:
        case "1":
            mostrar_catalogo()
        case "2":
            buscado = input("Título a buscar: ")
            if not buscado:
                print("No has escrito nada.")
                continue
            indice = buscar_libro(buscado)
            if indice is None:
                print(f"'{buscado}' no está en el catálogo.")
            else:
                print(f"'{catalogo[indice]}': {precios[indice]:.2f} €, stock {stocks[indice]}.")
        case "3":
            print(f"Cerrando {NOMBRE_TIENDA}. ¡Hasta mañana, Ana!")
            break
        case _:
            print("Opción no válida. Elige 1, 2 o 3.")

Fíjate en las decisiones de diseño:

  1. mostrar_catalogo() presenta: imprime y devuelve None; el while queda legible — la opción 1 es ahora una sola línea que se lee como una frase.
  2. buscar_libro() calcula: sustituye el patrón for/break/else de la versión anterior por algo aún más directo — return i corta la búsqueda en cuanto hay coincidencia (hace el papel del break) y el return None final cubre el caso "no encontrado" (el papel del else del bucle). Devolver el índice en lugar de imprimir permite que quien llama decida qué hacer con él; nos lo agradeceremos en la lección 03-04.
  3. if indice is None distingue "no encontrado" de un resultado válido; ojo, if not indice sería un bug, porque el índice 0 (La Odisea) también es falsy — truthiness del módulo 2 aplicada.
  4. Las funciones leen las globales catalogo, precios y stocks pero no las modifican: uso legítimo del ámbito global. Cuando extraigamos estas funciones a un módulo propio (03-04) verás cómo eliminar también esa dependencia.

El comportamiento del menú es idéntico al del módulo 2 — esa es la gracia de refactorizar — pero ahora la búsqueda es reutilizable: ticket.py o venta_socio.py podrán usar buscar_libro() en cuanto sepamos compartir código entre archivos (lección 03-04).

Errores Comunes y Consejos

  • Llamar sin paréntesis: mostrar_catalogo (sin ()) no ejecuta nada ni da error; simplemente nombra la función. Si "la función no hace nada", revisa los paréntesis.
  • Usar la función antes de definirla: Python lee el archivo de arriba abajo; llamar a buscar_libro() en una línea anterior a su def produce NameError. Convención práctica: constantes y datos arriba, luego los def, y el programa principal al final.
  • Confundir print con return: si total = precio_socio(9.95) te da None, casi seguro que la función imprime en lugar de devolver. Repasa la tabla comparativa de esta lección.
  • Código después del return: cualquier línea tras un return que siempre se ejecuta es código muerto. VS Code lo atenúa visualmente; bórralo o replantea la lógica.
  • Abusar de global: si te descubres escribiendo global en varias funciones, el diseño cojea. Pasa los datos como parámetros y devuelve los resultados.
  • Olvidar la docstring: para funciones de proyecto (las que otros —o tú dentro de un mes— reutilizarán), una línea de docstring es el mínimo educado.
  • Consejo: una función debe hacer una cosa. Si al describirla necesitas la palabra "y" ("muestra el catálogo y pide un libro y lo busca"), probablemente son dos o tres funciones.

Ejercicios

Ejercicio 1: ficha con y sin return

Escribe dos funciones: hay_stock(unidades), que reciba un número de unidades y devuelva True si es mayor que 0 y False en caso contrario; y mostrar_disponibilidad(titulo, unidades), que use la primera para imprimir '<titulo>': disponible o '<titulo>': agotado. Pruébalas con Hamlet (6 unidades) y Fausto (0 unidades). Añade docstring a ambas.

Ejercicio 2: contar agotados sin tocar globales

Dada la lista global stocks = [4, 6, 8, 0], escribe contar_agotados(lista_stocks) que reciba una lista de stocks por parámetro y devuelva cuántos valen 0 (patrón contador del módulo 2). Llámala con stocks e imprime Libros agotados: 1. Comprueba que la función funciona igual con [0, 0, 3]. ¿Por qué es mejor recibir la lista como parámetro que leer la global directamente?

Ejercicio 3: el detective del ámbito

Sin ejecutarlo, predice qué imprime este script y por qué. Después ejecútalo y comprueba tu hipótesis:

mensaje = "caja cerrada"

def abrir_caja():
    mensaje = "caja abierta"
    return mensaje

print(abrir_caja())
print(mensaje)

Soluciones

Ejercicio 1:

def hay_stock(unidades):
    """Devuelve True si quedan unidades disponibles."""
    return unidades > 0

def mostrar_disponibilidad(titulo, unidades):
    """Imprime si un título está disponible o agotado."""
    if hay_stock(unidades):
        print(f"'{titulo}': disponible")
    else:
        print(f"'{titulo}': agotado")

mostrar_disponibilidad("Hamlet", 6)   # 'Hamlet': disponible
mostrar_disponibilidad("Fausto", 0)   # 'Fausto': agotado

Observa que return unidades > 0 devuelve directamente el booleano de la comparación: no hace falta if unidades > 0: return True else: return False. Y una función puede llamar a otra: es la base de construir programas por capas.

Ejercicio 2:

stocks = [4, 6, 8, 0]

def contar_agotados(lista_stocks):
    """Devuelve cuántos elementos de la lista valen 0."""
    agotados = 0
    for unidades in lista_stocks:
        if unidades == 0:
            agotados += 1
    return agotados

print(f"Libros agotados: {contar_agotados(stocks)}")    # 1
print(contar_agotados([0, 0, 3]))                       # 2

Recibir la lista como parámetro hace la función reutilizable con cualquier lista (la de Papyrus hoy, la de otra tienda mañana, una lista de prueba en un test) y deja claro en la llamada qué datos usa. La variable agotados es local: no ensucia el script.

Ejercicio 3: Imprime caja abierta y después caja cerrada. La asignación mensaje = "caja abierta" dentro de la función crea una variable local que oculta a la global mientras la función corre; la global nunca cambia. Para que la función modificara la global haría falta global mensaje — que, como hemos visto, es mejor evitar devolviendo el valor (justo lo que hace el return).

Conclusión

Ya sabes crear funciones con def, distinguir parámetros (entrada) de return (salida), documentarlas con docstrings y razonar sobre el ámbito de sus variables: locales que nacen y mueren con cada llamada, globales que se pueden leer (constantes) pero que no conviene modificar con global. Con ello, menu.py ha dado su primer paso de "script suelto" a "programa organizado": mostrar_catalogo() y buscar_libro() tienen nombre, docstring y una única responsabilidad. Pero nuestras funciones aún son rígidas: buscar_libro() recibe un único argumento posicional y no hay manera de darle opciones. En la siguiente lección exploraremos a fondo los argumentos de función — con nombre, valores por defecto, cantidades variables con *args y **kwargs — y saldaremos una deuda pendiente del módulo 2: entender de verdad zip(), la herramienta que recorre en paralelo catalogo, precios y stocks.

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