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
| Aspecto | Posicional | Keyword |
|---|---|---|
| Orden | Importa: 1º arg → 1º param | No importa si nombras |
| Legibilidad | Menor con muchos params | Mayor: ves el nombre del param |
| Flexibilidad | No puedes saltar params | Sí, solo pasas los que quieras |
| Uso típico | Parámetros obligatorios | Pará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
return | print |
|---|---|
| Devuelve un valor al llamador | Escribe 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) -> strpreparan 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
- Documentación oficial — Definición de funciones
- PEP 8 — Naming Conventions
- Real Python — Defining Your Own Python Function
- PEP 257 — Docstring Conventions
- PEP 484 — Type Hints
- FastAPI — Python Types Intro
- Real Python — Type Hints in Python
- 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.