Módulo 2: Funciones, Módulos y Manejo de Errores

2. Funciones: Definición, Parámetros y Return

Descripción de la cápsula

En el Módulo 1 escribiste scripts lineales: variables, listas, condicionales, bucles. Pero el código backend real no es un archivo de 500 líneas ejecutándose de arriba a abajo. Está organizado en funciones: bloques reutilizables que encapsulan una responsabilidad, reciben datos, los procesan y devuelven un resultado. Cada endpoint de una API es una función. Cada validación, cada formateo, cada cálculo — si lo haces bien — vive dentro de una función.

Esta es tu primera exposición formal a funciones en Python. Vas a aprender por qué existen, cómo definirías con la palabra clave def, cómo manejar parámetros (posicionales, keyword, valores por defecto), cómo retornar valores (incluyendo múltiples valores con tuple unpacking) y cómo documentar tus funciones con docstrings. También verás type hints aplicados a funciones — la base de lo que FastAPI usa para generar schemas automáticamente.

Al terminar, podrás estructurar tu código como un profesional: cada función hace una cosa, la hace bien, y se puede reutilizar donde haga falta.


¿Por qué funciones?

En backend, repetir código no solo es tedioso — es fuente de bugs. Si validas un email en 5 endpoints distintos copiando las mismas líneas, el día que cambies la regla (por ejemplo: aceptar dominios nuevos) tendrás que actualizar 5 sitios. Si lo encapsulas en validate_email(texto), actualizas una sola función y todos los endpoints se benefician.

Las funciones te dan:

  • Reutilización: Escribes una vez, llamas en muchos lugares.
  • Organización: Un archivo de 500 líneas se vuelve legible cuando está partido en 15 funciones de ~30 líneas.
  • Legibilidad: Un nombre descriptivo como calculate_price(base, tax, discount) comunica la intención sin leer la implementación.

En backend, cada función suele tener una responsabilidad. get_user(id) obtiene el usuario. validate_input(data) valida. format_response(payload) formatea. Si una función hace demasiado, la divides.

# ❌ Sin funciones: lógica repetida
email1 = "usuario@dominio.com"
if "@" in email1 and "." in email1.split("@")[-1]:
    print("Email 1 válido")

email2 = "otro@ejemplo.org"
if "@" in email2 and "." in email2.split("@")[-1]:
    print("Email 2 válido")

# ✅ Con funciones: una definición, múltiples usos
def validate_email(texto: str) -> bool:
    return "@" in texto and "." in texto.split("@")[-1]

if validate_email("usuario@dominio.com"):
    print("Email 1 válido")
if validate_email("otro@ejemplo.org"):
    print("Email 2 válido")
# Output: Email 1 válido
# Output: Email 2 válido

Definir funciones: def y naming

Una función se define con la palabra clave def, seguida del nombre, paréntesis con parámetros (opcionales) y dos puntos. El cuerpo va indentado.

def saludar():
    print("Hola, bienvenido al backend")

saludar()
# Output: Hola, bienvenido al backend

Convenciones de nombres

En Python las funciones usan snake_case y preferiblemente verbos que describan la acción:

# ✅ Correcto: verbos descriptivos, snake_case
def get_user(user_id: int):
    pass

def validate_input(data: dict):
    pass

def format_response(payload: dict):
    pass

def calculate_price(base: float, tax: float, discount: float):
    pass

# ❌ Evitar: nombres vagos o camelCase
def getUser():      # camelCase — no es Pythonic
    pass

def do_stuff():     # vago — ¿qué hace?
    pass

Parámetros

Parámetros posicionales

Los parámetros que defines en orden son posicionales: el primer argumento se asigna al primer parámetro, el segundo al segundo, y así sucesivamente.

def greet(name: str, greeting: str) -> str:
    return f"{greeting}, {name}"

resultado = greet("Ana", "Hola")
print(resultado)
# Output: Hola, Ana

resultado = greet("Carlos", "Bienvenido")
print(resultado)
# Output: Bienvenido, Carlos

Valores por defecto

Puedes dar valores por defecto a parámetros. Los parámetros con default deben ir después de los que no tienen default.

def greet(name: str, greeting: str = "Hola") -> str:
    return f"{greeting}, {name}"

print(greet("Ana"))
# Output: Hola, Ana

print(greet("Ana", "Hi"))
# Output: Hi, Ana

# Parámetro con default puede omitirse o pasarse explícitamente
print(greet("Carlos"))
# Output: Hola, Carlos

Argumentos keyword (keyword arguments)

Puedes pasar argumentos por nombre. Eso te permite cambiar el orden o saltarte parámetros con default.

def greet(name: str, greeting: str = "Hola", emoji: str = "!") -> str:
    return f"{greeting}, {name}{emoji}"

# Por posición
print(greet("Ana", "Hi"))
# Output: Hi, Ana!

# Por keyword — orden no importa
print(greet(greeting="Bienvenido", name="Carlos", emoji="."))
# Output: Bienvenido, Carlos.

# Mezcla: posicionales primero, luego keywords
print(greet("Elena", greeting="Hey", emoji=" 👋"))
# Output: Hey, Elena 👋

Requeridos vs opcionales

  • Requeridos: parámetros sin valor por defecto. Deben pasarse siempre.
  • Opcionales: parámetros con = valor. Si no los pasas, se usa el default.
def create_user(name: str, email: str, plan: str = "free", active: bool = True) -> dict:
    return {"name": name, "email": email, "plan": plan, "active": active}

# name y email son requeridos; plan y active son opcionales
usuario1 = create_user("Ana", "ana@dev.io")
print(usuario1)
# Output: {'name': 'Ana', 'email': 'ana@dev.io', 'plan': 'free', 'active': True}

usuario2 = create_user("Carlos", "carlos@dev.io", plan="premium")
print(usuario2)
# Output: {'name': 'Carlos', 'email': 'carlos@dev.io', 'plan': 'premium', 'active': True}

Valores de retorno (return)

return explícito

La sentencia return termina la función y devuelve un valor al llamador.

def add(a: int, b: int) -> int:
    return a + b

resultado = add(3, 5)
print(resultado)
# Output: 8

Retorno implícito None

Si una función no tiene return o tiene return sin valor, devuelve None.

def log_message(msg: str) -> None:
    print(f"[LOG] {msg}")

resultado = log_message("Servidor iniciado")
print(resultado)
# Output: [LOG] Servidor iniciado
# Output: None

Retornar múltiples valores (tuple unpacking)

Python permite retornar varios valores separados por coma. Internamente se empaquetan en una tupla y puedes desempaquetarlos al llamar.

def parse_ids(ruta: str) -> tuple[int | None, int | None]:
    """Ejemplo: /api/users/42/orders/7 → (42, 7)"""
    partes = [p for p in ruta.strip("/").split("/") if p]
    user_id = int(partes[2]) if len(partes) > 2 and partes[1] == "users" else None
    order_id = int(partes[4]) if len(partes) > 4 and partes[3] == "orders" else None
    return (user_id, order_id)

user_id, order_id = parse_ids("/api/users/42/orders/7")
print(f"user_id={user_id}, order_id={order_id}")
# Output: user_id=42, order_id=7

# Sin unpacking, recibes la tupla completa
par = parse_ids("/api/users/10/orders/3")
print(par)
# Output: (10, 3)

Patrón early return

Salir de la función en cuanto detectas un caso que no quieres procesar. Evita anidamiento profundo y mejora la legibilidad.

def get_status_message(code: int) -> str:
    if code < 200:
        return "Informativo"
    if code < 300:
        return "Éxito"
    if code < 400:
        return "Redirección"
    if code < 500:
        return "Error del cliente"
    return "Error del servidor"

print(get_status_message(404))
# Output: Error del cliente

print(get_status_message(201))
# Output: Éxito

Type hints en funciones

Python admite anotar tipos en parámetros y en el valor de retorno. No cambian el comportamiento en runtime, pero herramientas como mypy y FastAPI los usan para validación y documentación.

def add(a: int, b: int) -> int:
    return a + b

def format_response(data: dict, status: int = 200) -> dict:
    return {"status": status, "data": data}

def validate_email(texto: str) -> bool:
    return "@" in texto and "." in texto.split("@")[-1]

FastAPI usa estos type hints para:

  • Validar automáticamente el body y los query params
  • Generar documentación OpenAPI
  • Serializar respuestas

Introduce type hints desde ya en tus funciones. En este path los usarás en todo el código.


Docstrings

Los docstrings son cadenas en triple comilla justo después de la definición de la función. Documentan qué hace la función, sus parámetros y su retorno.

def calculate_price(base: float, tax: float = 0.16, discount: float = 0.0) -> float:
    """
    Calcula el precio final aplicando impuesto y descuento.

    Args:
        base: Precio base del producto.
        tax: Tasa de impuesto (ej: 0.16 para 16%). Default 0.16.
        discount: Descuento aplicado (ej: 0.10 para 10%). Default 0.

    Returns:
        Precio final redondeado a 2 decimales.
    """
    subtotal = base * (1 + tax)
    return round(subtotal * (1 - discount), 2)

precio = calculate_price(100.0, discount=0.10)
print(f"Precio final: ${precio}")
# Output: Precio final: $104.4

En backend, los docstrings ayudan a que otros devs (o tú en 6 meses) entiendan el contrato de la función sin leer el código. Herramientas como Sphinx los usan para generar documentación automática.


Llamar funciones: posicional vs keyword

Puedes llamar funciones con argumentos posicionales, con keywords, o mezclando ambos. La regla: primero los posicionales, luego los keyword.

def config_server(host: str, port: int = 8000, debug: bool = False) -> str:
    return f"{host}:{port} (debug={debug})"

# Solo posicionales
print(config_server("0.0.0.0"))
# Output: 0.0.0.0:8000 (debug=False)

# Posicional + keyword
print(config_server("localhost", port=3000))
# Output: localhost:3000 (debug=False)

# Solo keywords — orden libre
print(config_server(debug=True, host="127.0.0.1", port=5000))
# Output: 127.0.0.1:5000 (debug=True)

Funciones como objetos de primera clase

En Python las funciones son objetos. Puedes asignarlas a variables, pasarlas como argumentos y retornarlas. Eso permite patrones como callbacks, estrategias y decoradores (que verás en guías posteriores).

def validate_email(texto: str) -> bool:
    return "@" in texto and "." in texto.split("@")[-1]

def validate_positive(value: float) -> bool:
    return value > 0

# Almacenar en variable
validator = validate_email
print(validator("user@dev.io"))
# Output: True

# Pasar como argumento
def apply_validator(fn, value):
    return fn(value)

print(apply_validator(validate_email, "bad-email"))
# Output: False

print(apply_validator(validate_positive, 42.5))
# Output: True

No profundizamos aquí; lo retomarás con decoradores y dependencias en FastAPI.


Comparación: Parámetros posicionales vs keyword

AspectoPosicionalKeyword
OrdenImporta: 1º arg → 1º paramNo importa si nombras
LegibilidadMenor con muchos paramsMayor: ves el nombre del param
FlexibilidadNo puedes saltar paramsSí, solo pasas los que quieras
Uso típicoParámetros obligatoriosParámetros opcionales o muchos params
def create_endpoint(method: str, path: str, handler: str, auth: bool = True):
    return f"{method} {path}{handler} (auth={auth})"

# Posicional: rápido pero menos claro con muchos params
# create_endpoint("GET", "/users", "get_users", False)

# Keyword: claro y explícito
print(create_endpoint(method="GET", path="/users", handler="get_users", auth=False))
# Output: GET /users → get_users (auth=False)

Comparación: return vs print

returnprint
Devuelve un valor al llamadorEscribe en la consola
Útil para reutilizar el resultadoÚtil para debug o logs
Una función puede retornar y además imprimir (menos común)No reemplaza return
def bad_calc(a: int, b: int):
    print(a + b)  # ❌ Solo imprime, no devuelve

def good_calc(a: int, b: int) -> int:
    return a + b  # ✅ Devuelve para que el llamador use el valor

x = bad_calc(3, 5)
print(f"Resultado: {x}")
# Output: 8
# Output: Resultado: None

y = good_calc(3, 5)
print(f"Resultado: {y}")
# Output: Resultado: 8

En backend, las funciones suelen retornar valores; el código que las llama decide si los imprime, los envía por HTTP o los guarda en base de datos.


Conexión con el proyecto

Las funciones que defines aquí serán la base del CLI del Módulo 3 y de cualquier API que construyas después. Patrones típicos:

def validate_email(email: str) -> bool:
    """Valida formato básico de email."""
    return "@" in email and "." in email.split("@")[-1]

def format_response(data: dict, status: int = 200) -> dict:
    """Estructura estándar de respuesta de API."""
    return {"status": status, "data": data}

def calculate_price(base: float, tax: float = 0.16, discount: float = 0.0) -> float:
    """Calcula precio final con impuesto y descuento."""
    return round(base * (1 + tax) * (1 - discount), 2)

# Uso típico en un endpoint simulado
user_input = {"email": "ana@dev.io", "base_price": 99.99}
if not validate_email(user_input["email"]):
    print("Error: email inválido")
else:
    precio = calculate_price(user_input["base_price"], discount=0.10)
    respuesta = format_response({"email": user_input["email"], "total": precio})
    print(respuesta)
# Output: {'status': 200, 'data': {'email': 'ana@dev.io', 'total': 103.19}}

Troubleshooting

Problema 1: TypeError por argumentos faltantes

def greet(name: str, greeting: str) -> str:
    return f"{greeting}, {name}"

# Error
# greet("Ana")
# TypeError: greet() missing 1 required positional argument: 'greeting'

Causa: pasaste menos argumentos de los requeridos.

Solución: pasa todos los parámetros obligatorios o dales valor por defecto.

def greet(name: str, greeting: str = "Hola") -> str:
    return f"{greeting}, {name}"

print(greet("Ana"))
# Output: Hola, Ana

Problema 2: Parámetros con default antes que los requeridos

# ❌ SyntaxError
# def config(host: str = "localhost", port: int):
#     pass
# SyntaxError: non-default argument follows default argument

Causa: en la firma, los parámetros sin default deben ir antes de los que tienen default.

Solución:

def config(port: int, host: str = "localhost") -> str:
    return f"{host}:{port}"

print(config(8000))
# Output: localhost:8000

Problema 3: Modificar mutable como valor por defecto

# ❌ Peligro: lista mutable como default
def add_item(item, lista=[]):
    lista.append(item)
    return lista

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2] ← ¡la misma lista se reutiliza!

Causa: el objeto por defecto se crea una sola vez al definir la función. Si es mutable, se comparte entre llamadas.

Solución: usa None y crea la lista dentro de la función.

def add_item(item, lista: list | None = None) -> list:
    if lista is None:
        lista = []
    lista.append(item)
    return lista

print(add_item(1))  # [1]
print(add_item(2))  # [2] — lista nueva en cada llamada
# Output: [1]
# Output: [2]

Problema 4: Confundir return con print

def double(x: int):
    print(x * 2)  # Solo imprime, no retorna

resultado = double(5)
print(resultado)  # None
# Output: 10
# Output: None

Solución: usa return cuando necesites el valor en el llamador.

def double(x: int) -> int:
    return x * 2

resultado = double(5)
print(resultado)
# Output: 10

Problema 5: Keyword después de posicional en la misma llamada

def f(a: int, b: int, c: int) -> int:
    return a + b + c

# ❌ Error: keyword argument sigue a posicional
# f(1, b=2, 3)
# SyntaxError: positional argument follows keyword argument

Solución: los argumentos posicionales van primero; todos los keyword, después.

print(f(1, 2, c=3))
# Output: 6

Ejercicios

Ejercicio 1: validate_email (Fácil)

Implementa validate_email(texto: str) -> bool que retorne True si el texto tiene @ y un . después del @. Prueba con varios ejemplos válidos e inválidos.

Ver solución
def validate_email(texto: str) -> bool:
    if not texto or "@" not in texto:
        return False
    parte_dominio = texto.split("@")[-1]
    return "." in parte_dominio

print(validate_email("user@dominio.com"))
# Output: True

print(validate_email("user@dominio"))
# Output: False

print(validate_email("userdominio.com"))
# Output: False

print(validate_email(""))
# Output: False

Ejercicio 2: format_response (Fácil)

Crea format_response(data: dict, status: int = 200, message: str | None = None) -> dict. Retorna un dict con status, data y message (solo si se pasa). Incluye type hints y docstring.

Ver solución
def format_response(
    data: dict,
    status: int = 200,
    message: str | None = None
) -> dict:
    """
    Formatea la respuesta estándar de una API.
    """
    resultado: dict = {"status": status, "data": data}
    if message is not None:
        resultado["message"] = message
    return resultado

print(format_response({"user": "ana"}))
# Output: {'status': 200, 'data': {'user': 'ana'}}

print(format_response({"error": "Not found"}, status=404, message="Recurso no encontrado"))
# Output: {'status': 404, 'data': {'error': 'Not found'}, 'message': 'Recurso no encontrado'}

Ejercicio 3: calculate_price (Intermedio)

Implementa calculate_price(base: float, tax: float = 0.16, discount: float = 0.0) -> float que calcule base * (1 + tax) * (1 - discount) redondeado a 2 decimales. Verifica con varios casos.

Ver solución
def calculate_price(
    base: float,
    tax: float = 0.16,
    discount: float = 0.0
) -> float:
    return round(base * (1 + tax) * (1 - discount), 2)

print(calculate_price(100.0))
# Output: 116.0

print(calculate_price(100.0, discount=0.10))
# Output: 104.4

print(calculate_price(50.0, tax=0.21, discount=0.05))
# Output: 57.48

Ejercicio 4: parse_pagination (Intermedio)

Crea parse_pagination(page: int = 1, per_page: int = 20) -> tuple[int, int] que retorne (offset, limit) para usar en una query. Valida que page y per_page sean positivos; si no, retorna (0, 20).

Ver solución
def parse_pagination(page: int = 1, per_page: int = 20) -> tuple[int, int]:
    if page < 1 or per_page < 1:
        return (0, 20)
    offset = (page - 1) * per_page
    return (offset, per_page)

offset, limit = parse_pagination(3, 10)
print(f"offset={offset}, limit={limit}")
# Output: offset=20, limit=10

offset, limit = parse_pagination()
print(f"offset={offset}, limit={limit}")
# Output: offset=0, limit=20

offset, limit = parse_pagination(0, 5)
print(f"offset={offset}, limit={limit}")
# Output: offset=0, limit=20

Ejercicio 5: get_user_summary (Intermedio)

Implementa get_user_summary(user: dict) -> str que reciba un dict con name, email, plan (opcional, default "free") y retorne un string formateado: "Nombre (email) — Plan: X". Usa early return si faltan campos obligatorios.

Ver solución
def get_user_summary(user: dict) -> str:
    if "name" not in user or "email" not in user:
        return "Usuario inválido: faltan name o email"
    plan = user.get("plan", "free")
    return f"{user['name']} ({user['email']}) — Plan: {plan}"

print(get_user_summary({"name": "Ana", "email": "ana@dev.io"}))
# Output: Ana (ana@dev.io) — Plan: free

print(get_user_summary({"name": "Carlos", "email": "carlos@dev.io", "plan": "premium"}))
# Output: Carlos (carlos@dev.io) — Plan: premium

print(get_user_summary({"name": "Elena"}))
# Output: Usuario inválido: faltan name o email

Ejercicio 6: Múltiples retornos con early return (Avanzado)

Crea validate_request_body(body: dict, required: list[str]) -> tuple[bool, list[str]] que retorne (True, []) si todo está bien, o (False, lista_de_errores) si faltan campos requeridos. Usa early return cuando encuentres el primer error para una versión; luego haz otra que recoja todos los errores.

Ver solución
def validate_request_body(body: dict, required: list[str]) -> tuple[bool, list[str]]:
    errores: list[str] = []
    for campo in required:
        if campo not in body or body[campo] is None:
            errores.append(f"Campo requerido faltante: '{campo}'")
    if errores:
        return (False, errores)
    return (True, [])

ok, errs = validate_request_body({"name": "Ana", "email": "ana@dev.io"}, ["name", "email"])
print(f"ok={ok}, errores={errs}")
# Output: ok=True, errores=[]

ok, errs = validate_request_body({"name": "Carlos"}, ["name", "email"])
print(f"ok={ok}, errores={errs}")
# Output: ok=False, errores=["Campo requerido faltante: 'email'"]

Resumen

  • Funciones encapsulan lógica reutilizable: def, nombre en snake_case, verbos descriptivos.
  • Parámetros: posicionales, keyword, valores por defecto. Requeridos antes que opcionales.
  • return devuelve valor al llamador; sin return la función devuelve None.
  • Múltiples valores: retornas una tupla y desempaquetas: a, b = fn().
  • Early return: salir pronto en casos inválidos reduce anidamiento.
  • Type hints: def f(x: int) -> str preparan el terreno para FastAPI.
  • Docstrings: documentan contrato y uso; esencial en código compartido.
  • Funciones son objetos: puedes pasarlas como argumentos y guardarlas en variables.

Recursos adicionales

  1. Documentación oficial — Definición de funciones
  2. PEP 8 — Naming Conventions
  3. Real Python — Defining Your Own Python Function
  4. PEP 257 — Docstring Conventions
  5. PEP 484 — Type Hints
  6. FastAPI — Python Types Intro
  7. Real Python — Type Hints in Python
  8. Documentación — Built-in Functions

Tiempo estimado: 45–60 minutos
Siguiente cápsula: 03-funciones-avanzadas.md*args, **kwargs, scope, closures y decoradores básicos.