🐍 Curso Python · Nivel N2 Básico

Bloque 3: Funciones, módulos y ficheros

Hasta ahora escribías programas de arriba abajo. Ahora aprenderás a porcionarlo en piezas reutilizables: funciones con argumentos flexibles (*args, **kwargs, lambda), módulos e imports, gestión de errores con try/except y lectura/escritura de ficheros. Terminarás con un gestor de gastos que guarda datos en disco: tu primera aplicación «de verdad».

⏱ ≈ 17 horas 📚 5 lecciones ✍️ 15 ejercicios guiados 📝 Examen del bloque + gestor de gastos
Lección 1 · Teoría

Funciones: define y reutiliza

Una función es un bloque de código con nombre que recibe valores (argumentos), hace algo y devuelve un resultado. Se define con def y se invoca con paréntesis. Es la pieza básica para no repetir código.

definir y llamar una función
def area_circulo(radio):
    # return devuelve el resultado al que llama
    return 3.1416 * radio ** 2

a = area_circulo(5)
print(f"{a:.2f} u²")   # 78.54 u²

1.1 · Parámetros y valores por defecto

Un parámetro con = valor es opcional. Los opcionales siempre van después de los obligatorios:

parámetros por defecto
def saludar(nombre, idioma="es"):
    if idioma == "es":
        return f"Hola, {nombre}"
    return f"Hello, {nombre}"

print(saludar("Ana"))          # Hola, Ana (usa el default)
print(saludar("Ana", "en"))  # Hello, Ana (lo pasamos explícito)
⚠️ Si una función no tiene return, devuelve None. Muchas funciones «hacen cosas» (como print()) y no devuelven nada útil: no intentes guardar su resultado.

1.2 · Argumentos por nombre (keywords)

Puedes llamar a una función pasando los valores en orden (posicionales) o indicando el nombre del parámetro. Por nombre es más legible y no dependes del orden:

argumentos posicionales vs por nombre
def crear_usuario(nombre, email, rol="alumno"):
    return {"nombre": nombre, "email": email, "rol": rol}

u1 = crear_usuario("Luis", "l@x.es")
u2 = crear_usuario(email="r@x.es", nombre="Rita", rol="admin")
Lección 2 · Teoría

Argumentos avanzados y lambda

2.1 · *args: un número variable de posicionales

*args recoge TODOS los argumentos posicionales en una tupla. Ideal para sumar n números:

suma de n valores
def suma_total(*args):
    return sum(args)

print(suma_total(1, 2))        # 3
print(suma_total(1, 2, 3, 4))  # 10

2.2 · **kwargs: argumentos nombrados variables

**kwargs recoge los argumentos por nombre en un diccionario:

config opcional por nombre
def configurar(**kwargs):
    for clave, valor in kwargs.items():
        print(f"{clave} = {valor}")

configurar(host="localhost", puerto=5432, modo="debug")

2.3 · Lambda: funciones pequeñas y anónimas

Una lambda es una función de una sola expresión, sin def ni return. Se usa sobre todo como «clave» de sorted, min, max o con map/filter:

lambdas en acción
personas = [("Ana", 25), ("Luis", 40), ("Rita", 30)]

# ordenar por la edad (segundo elemento de cada tupla)
ordenadas = sorted(personas, key=lambda persona: persona[1])

# la persona más joven
mas_joven = min(personas, key=lambda p: p[1])

print(ordenadas)   # [('Ana', 25), ('Rita', 30), ('Luis', 40)]
print(mas_joven)   # ('Ana', 25)
💡 Guía rápida: lambda x: x[1] significa «fungión que recibe x y devuelve x[1]». La sintaxis es lambda parámetros: expresión.

2.4 · Funciones como ciudadanas de primera clase

En Python las funciones son valores: puedes guardarlas en variables, pasarlas como argumento y devolverlas. Eso abre la puerta a la programación funcional:

pasar funciones como argumento
def aplicar(funcion, valor):
    return funcion(valor)

def doble(x):
    return x * 2

print(aplicar(doble, 5))         # 10
print(aplicar(lambda x: x * x, 3)) # 9
Lección 3 · Teoría

Ámbitos, docstrings y buenas prácticas

3.1 · Ámbito local y global

Las variables creadas dentro de una función son locales (viven solo ahí). Las creadas al nivel superior del programa son globales. Para leer una global desde una función no hace falta nada; para modificarla, necesitas global:

ámbito de variables
contador = 0   # global

def incrementar():
    global contador        # indica que vamos a modificar la global
    contador += 1

incrementar()
incrementar()
print(contador)   # 2
⚠️ Abusar de global es mala práctica. Lo ideal es que las funciones reciban valores por parámetros y devuelvan resultados. ¿Quieres una lista global modificada? Pásala como argumento o devuélvela. Las globals hacen el código difícil de seguir y de probar.

3.2 · Docstrings: la documentación en el código

Un docstring es un texto con comillas triples justo después de def. Explica QUÉ hace la función, qué parámetros espera y qué devuelve. Herramientas como help() lo leen:

función documentada
def precio_con_iva(precio, iva=15):
    """Devuelve el precio con el IVA aplicado y redondeado a 2 decimales.

    Args:
        precio (float): precio base
        iva (int): porcentaje de IVA (por defecto 15)

    Returns:
        float: precio final con IVA
    """
    return round(precio * (1 + iva / 100), 2)

print(precio_con_iva(100))      # 115.0
print(precio_con_iva(100, 21))  # 121.0

3.3 · Buenas prácticas

Una función = una tarea. Si hay que hacer dos cosas, parte en dos.
Nombres acción-verbo: calcular_total(), guardar(), nunca cosas().
Realmente return el resultado en lugar de solo printar (más fácil de probar).
No más de ~10-20 líneas por función. Si crece, refactoriza.
Usa defaults y keyword args para API flexibles y legibles.
Nunca from modulo import *: importa lo que necesites.
Lección 4 · Teoría

Módulos y excepciones

4.1 · Módulos: importar código

Un módulo es un archivo .py con funciones, clases y variables que puedes reutilizar en otros programas mediante import:

importar módulos
# módulos de la biblioteca estándar
import math
import random
import datetime as dt      # alias
from collections import Counter  # importar algo concreto

print(math.sqrt(16))             # 4.0
print(random.randint(1, 6))  # un número del 1 al 6
print(dt.date.today())          # 2026-09-01

También puedes importar tus propios archivos. Si creas utilidades.py con una función, otro archivo hace from utilidades import generar_id. Eso es lo que separa un script de un programa modular.

4.2 · pip: instalar librerías de terceros

pip instala paquetes del índice oficial (pypi.org):

gestionar paquetes
$ pip install requests        # instala un paquete
$ pip list                    # los paquetes instalados
$ pip uninstall requests      # desinstala
$ pip freeze                  # lista versiones (para guardar en requirements.txt)
💡 Siempre instala paquetes, no a pelo.

4.3 · Excepciones: errores controlados

En Python, cuando algo falla se lanza una excepción. Si no la capturas, el programa muere. Con try/except la controlas tú:

capturar y gestionar errores
texto = input("Edad: ")

try:
    edad = int(texto)
    print(f"Dentro de 10 años tendrás {edad + 10}")
except ValueError:
    print("Eso no es un número válido")

Excepciones habituales: ValueError (valor inválido para int()), KeyError (dict sin clave), IndexError (índice fuera de rango), FileNotFoundError (fichero inexistente), ZeroDivisionError. Puedes encadenar except con distintos tipos.

⚠️ No captures Exception a secas para «tapar» errores. Captura el tipo concreto, o usa else (si va bien) y finally (siempre se ejecuta, p. ej. para cerrar un archivo).
Lección 5 · Teoría

Ficheros: persistir datos

Para que un programa recuerde datos entre ejecuciones hay que guardarlos en disco. Python trabaja con ficheros de texto y con formatos estructurados como CSV y JSON.

5.1 · Leer y escribir con with

El bloque with cierra el archivo automáticamente al terminar, incluso si hay un error. Es la forma recomendada, nunca olvides cerrarlo:

escribir y leer un fichero
# escribir (w: sobrescribe, a: añade al final)
with open("notas.txt", "w", encoding="utf-8") as f:
    f.write("Línea uno\n")
    f.write("Línea dos\n")

# leer todo el contenido
with open("notas.txt", encoding="utf-8") as f:
    contenido = f.read()
    print(contenido)
💡 encoding="utf-8" es imprescindible para los acentos y la ñ en español. Sin él, Windows podría dar errores de codificación al leer tu archivo.

5.2 · Leer por líneas

iterar sobre líneas
with open("notas.txt", encoding="utf-8") as f:
    for linea in f:
        print(linea.strip())   # strip quita el salto de línea

5.3 · Trabajar con JSON

JSON es el formato universal para intercambiar datos. Python lo maneja con el módulo json:

guardar y recuperar un dict en JSON
import json

datos = {"nombre": "Ana", "edad": 25, "activo": True}

# guardar (serializar)
with open("usuario.json", "w", encoding="utf-8") as f:
    json.dump(datos, f, ensure_ascii=False, indent=2)

# leer (deserializar)
with open("usuario.json", encoding="utf-8") as f:
    recuperado = json.load(f)

print(recuperado["nombre"])   # Ana — y ya es un dict de Python
⚠️ Para procesar CSV, la biblioteca estándar tiene el módulo csv. Aunque lo verás en el bloque 5 a fondo, ya puedes hacer la idea: csv.reader y csv.DictWriter mueven tablas con una API parecida a la anterior.
🎉 Con funciones, import, excepciones y ficheros construyes aplicaciones reales: programas que piden datos, los procesan de forma organizada y los guardan para la próxima vez. Es el salto de «script» a «programa».
Manos a la obra

✍️ Los 15 ejercicios del bloque

Crea la carpeta python-b3/ y un archivo .py por ejercicio. Practica ejecutando cada programa; cuando un ejercicio pida funciones, define la función y luego pruébala con varios casos con print().

Sección A · Funciones básicas

1 Saludar con default

Define saludar(nombre, mensaje="Hola") que devuelva f"{mensaje}, {nombre}". Llámala dos veces: con el default y con mensaje personalizado.

Ver solución
def saludar(nombre, mensaje="Hola"):
    return f"{mensaje}, {nombre}"

print(saludar("Ana"))
print(saludar("Ana", "Buenos días"))

2 Triple función

Crea cuadrado(x), cubo(x) y factorial(n) (con un bucle). Prueba: cuadrado(3)=9, cubo(2)=8, factorial(5)=120.

Ver solución
def cuadrado(x):
    return x ** 2

def cubo(x):
    return x ** 3

def factorial(n):
    resultado = 1
    for i in range(2, n + 1):
        resultado *= i
    return resultado

print(cuadrado(3), cubo(2), factorial(5))

3 Palabra invertida

Define invertir(texto) que devuelva el texto invertido con slicing y prueba con "Python" → "nohtyP".

Ver solución
def invertir(texto):
    return texto[::-1]

print(invertir("Python"))  # nohtyP

4 Suma con *args

Define suma_varios(*args) que devuelva la suma de todos los argumentos usando sum(). Prueba con 2, 3 y 5 valores distintos.

Ver solución
def suma_varios(*args):
    return sum(args)

print(suma_varios(1, 2))              # 3
print(suma_varios(1, 2, 3))        # 6
print(suma_varios(1, 2, 3, 4, 5))  # 15

5 Media de notas con *args

Define media(*notas) que devuelva la media de las notas. Si no recibe ninguna, devuelve 0. Prueba con varias combinaciones.

Ver solución
def media(*notas):
    if not notas:
        return 0
    return sum(notas) / len(notas)

print(media(7, 8, 9))   # 8.0
print(media())              # 0

6 Imprimir configuración con **kwargs

Define mostrar_config(**kwargs) que recorra kwargs.items() y muestre clave = valor por línea. Prueba pasando 3 configuraciones.

Ver solución
def mostrar_config(**kwargs):
    for clave, valor in kwargs.items():
        print(f"{clave} = {valor}")

mostrar_config(host="localhost", puerto=8080, debug=True)

Sección B · Lambda

7 Ordenar personas por edad

Dada personas = [("Ana", 25), ("Luis", 40), ("Rita", 30)], ordénalas por edad con sorted + lambda. Muestra el resultado.

Ver solución
personas = [("Ana", 25), ("Luis", 40), ("Rita", 30)]
ordenadas = sorted(personas, key=lambda p: p[1])
print(ordenadas)  # [('Ana', 25), ('Rita', 30), ('Luis', 40)]

8 Más barato y más caro

Dada precios = {"ratón": 12, "teclado": 45, "pad": 8}, usa min y max con key=precios.get para mostrar el producto más barato y el más caro.

Ver solución
precios = {"ratón": 12, "teclado": 45, "pad": 8}
print(min(precios, key=precios.get))  # pad
print(max(precios, key=precios.get))  # teclado

Sección C · Excepciones y ficheros

9 Dividir con seguridad

Define dividir(a, b) que devuelva a / b pero capture en ZeroDivisionError devolviendo None. Prueba con dividir(10, 2) y dividir(10, 0).

Ver solución
def dividir(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return None

print(dividir(10, 2))   # 5.0
print(dividir(10, 0))   # None

10 Edad robusta

Usa try/except ValueError para convertir a entero la entrada de input(). Si falla, muestra «edad no válida»; si funciona, muestra f"Edad correcta: {edad}".

Ver solución
texto = input("Tu edad: ")
try:
    edad = int(texto)
    print(f"Edad correcta: {edad}")
except ValueError:
    print("edad no válida")

11 Escribir y leer

Crea un archivo mensaje.txt con open(..., "w"), escribe 3 líneas, y luego lee el contenido completo con with open(..., encoding="utf-8") y muéstralo.

Ver solución
with open("mensaje.txt", "w", encoding="utf-8") as f:
    f.write("primera línea\n")
    f.write("segunda línea\n")
    f.write("tercera línea\n")

with open("mensaje.txt", encoding="utf-8") as f:
    print(f.read())

12 Número de líneas

Dado mensaje.txt del ejercicio anterior, cuenta y muestra cuántas líneas tiene usando for linea in f. Debe salir 3.

Ver solución
cuenta = 0
with open("mensaje.txt", encoding="utf-8") as f:
    for linea in f:
        cuenta += 1
print(f"El fichero tiene {cuenta} líneas")

13 Guardar y leer JSON

Con json.dump guarda {"nombre": "Ana", "edad": 25} en usuario.json con indent. Después cárgalo con json.load y muestra nombre y edad.

Ver solución
import json

datos = {"nombre": "Ana", "edad": 25}
with open("usuario.json", "w", encoding="utf-8") as f:
    json.dump(datos, f, ensure_ascii=False, indent=2)

with open("usuario.json", encoding="utf-8") as f:
    carga = json.load(f)
print(carga["nombre"], carga["edad"])

14 Módulo propio

Crea utilidades.py con la función generar_id() que devuelva un número entero aleatorio del 1 al 1000 (usa random.randint). Crea otro archivo main.py que importe la función y genere 3 ids imprimiéndolos.

Ver solución
# utilidades.py
import random

def generar_id():
    return random.randint(1, 1000)

# main.py
from utilidades import generar_id

print(generar_id())
print(generar_id())
print(generar_id())

15 🏆 Proyecto integrador: «gestor de gastos»

Crea gastos.py: un programa que registra gastos y los persiste en un fichero JSON. Requisitos:

  • Carga los gastos existentes de gastos.json (con json.load; si no existe, empieza con una lista vacía dentro de un try/except FileNotFoundError).
  • Menú while True: 1 añadir gasto (concepto y importe con float), 2 ver la lista, 3 total acumulado, salir.
  • Usa una función guardar(gastos) que escriba con json.dump.
  • Al añadir, valida el importe con try/except ValueError.
  • Comentarios y nombres en snake_case.
Ver una solución posible
# Gestor de gastos persistente
import json

def guardar(gastos):
    with open("gastos.json", "w", encoding="utf-8") as f:
        json.dump(gastos, f, ensure_ascii=False, indent=2)

try:
    with open("gastos.json", encoding="utf-8") as f:
        gastos = json.load(f)
except FileNotFoundError:
    gastos = []

while True:
    print("\n1 añadir · 2 ver · 3 total · salir")
    opcion = input("Elige: ").strip().lower()

    if opcion == "1":
        concepto = input("Concepto: ")
        try:
            importe = float(input("Importe: "))
        except ValueError:
            print("Importe no válido")
            continue
        gastos.append({"concepto": concepto, "importe": importe})
        guardar(gastos)
        print(f"Añadido: {concepto} ({importe:.2f} €)")

    elif opcion == "2":
        for g in gastos:
            print(f"- {g['concepto']}: {g['importe']:.2f} €")

    elif opcion == "3":
        total = sum(g["importe"] for g in gastos)
        print(f"Total: {total:.2f} €")

    elif opcion == "salir":
        break
Evaluación del bloque

📝 Examen del Bloque 3

Dos partes: un test teórico autoevaluable (mínimo 70 %) y un reto práctico de autocorrección con laboratorio.

Parte A · Test teórico (aprueba con 11/15)

1 · ¿Qué palabra clave define una función?
2 · ¿Qué devuelve una función sin return?
3 · ¿Qué recoge *args?
4 · ¿Qué recoge **kwargs?
5 · ¿Qué es una lambda?
6 · ¿Qué excepción se lanza al hacer int("abc")?
7 · ¿Qué bloque cierra siempre un fichero aunque haya error?
8 · ¿Qué modo abre un fichero para sobrescribirlo?
9 · ¿Qué excepción se lanza si el fichero no existe al abrirlo?
10 · ¿Qué es un docstring?
11 · ¿Para qué sirve el parámetro encoding="utf-8"?
12 · ¿Qué hace json.dump(datos, f)?
13 · ¿Qué herramienta instala paquetes de terceros?
14 · ¿Cómo importas solo la función Counter de collections?
15 · ¿Qué problema resuelve una variable local?

Parte B · Reto «promedio calculadora»

Crea calculadora.py: funciones puras de cálculo (sumar, restar, multiplicar, dividir) más suma_varios(*args), una lambda de ordenación y un try/except. Valida tú mismo/a estos criterios. Mínimo para aprobar: 6 de 8 casillas.

Cumplidos: 0 / 8

🎉 ¿Superaste las dos partes? Bloque 3 completado. Ya estructuras programas con funciones, partes tu código en módulos, controlas errores y persistes datos en ficheros. En el bloque 4 darás el gran salto: programación orientada a objetos. 🐍🔥