Un conversor de monedas es un proyecto excelente para practicar entrada de datos, funciones, solicitudes HTTP, validación y manejo de errores. La aplicación recibe un importe, una moneda de origen y otra de destino, consulta una tasa actual y devuelve el valor convertido. Aunque el cálculo es sencillo, un programa confiable debe gestionar códigos inválidos, fallos de red, datos incompletos y fechas de actualización.
Antes de comenzar, revisa la biblioteca Requests en Python, el manejo de excepciones, las funciones, las variables de entorno y el trabajo con diccionarios.
Cómo funciona la conversión
La fórmula básica es:
resultado = importe * tasa
Si un euro equivale a 1,08 dólares, convertir 100 EUR produce 108 USD. La dificultad está en obtener una tasa adecuada y saber qué representa. Algunas APIs devuelven tasas respecto a una moneda base; otras permiten solicitar directamente el par deseado.
El Banco Central Europeo publica tipos de referencia para varias monedas. Para un proyecto educativo también puede utilizarse la documentación de la API Frankfurter, que ofrece datos basados en fuentes públicas. Ninguna tasa de referencia garantiza el importe exacto que cobrará un banco, una tarjeta o una casa de cambio.
Preparar el entorno
python -m venv .venv
.venv\Scripts\activate
python -m pip install requests
En macOS o Linux activa el entorno con source .venv/bin/activate. Evita instalar dependencias globalmente, porque distintos proyectos pueden requerir versiones diferentes.
Primera consulta a la API
import requests
url = "https://api.frankfurter.app/latest"
parametros = {
"amount": 100,
"from": "EUR",
"to": "USD",
}
respuesta = requests.get(url, params=parametros, timeout=10)
respuesta.raise_for_status()
datos = respuesta.json()
print(datos)
El parámetro timeout evita que el programa espere indefinidamente. raise_for_status() transforma respuestas HTTP de error en excepciones que pueden manejarse.
Extraer el resultado
resultado = datos["rates"]["USD"]
fecha = datos["date"]
print(f"100 EUR = {resultado:.2f} USD")
print(f"Tasa de referencia: {fecha}")
No asumas que todas las claves existen. Una respuesta de error puede tener una estructura distinta. Utiliza acceso seguro y valida tipos.
Crear una función reutilizable
from decimal import Decimal, InvalidOperation
import requests
API_URL = "https://api.frankfurter.app/latest"
def convertir_moneda(importe, origen, destino):
origen = origen.strip().upper()
destino = destino.strip().upper()
if len(origen) != 3 or len(destino) != 3:
raise ValueError("Los códigos deben tener tres letras")
if origen == destino:
return {
"resultado": Decimal(str(importe)),
"fecha": None,
"origen": origen,
"destino": destino,
}
parametros = {
"amount": str(importe),
"from": origen,
"to": destino,
}
respuesta = requests.get(API_URL, params=parametros, timeout=10)
respuesta.raise_for_status()
datos = respuesta.json()
tasas = datos.get("rates", {})
if destino not in tasas:
raise ValueError("La respuesta no contiene la moneda solicitada")
return {
"resultado": Decimal(str(tasas[destino])),
"fecha": datos.get("date"),
"origen": origen,
"destino": destino,
}
Decimal ayuda a conservar una representación decimal controlada. Aun así, la precisión final depende de la tasa proporcionada por el servicio.
Validar la entrada del usuario
def leer_importe():
while True:
texto = input("Importe: ").strip().replace(",", ".")
try:
valor = Decimal(texto)
if valor < 0:
print("El importe no puede ser negativo")
continue
return valor
except InvalidOperation:
print("Introduce un número válido")
En una aplicación real debes decidir si el valor cero es aceptable y qué límites aplicar. Los códigos de moneda deben compararse con una lista admitida, no solo por longitud.
Obtener las monedas disponibles
def obtener_monedas():
respuesta = requests.get(
"https://api.frankfurter.app/currencies",
timeout=10,
)
respuesta.raise_for_status()
datos = respuesta.json()
if not isinstance(datos, dict):
raise ValueError("Formato inesperado")
return datos
Esta información puede almacenarse temporalmente porque no cambia con frecuencia. Así evitas una solicitud adicional en cada conversión.
Programa de terminal completo
def main():
print("Conversor de monedas")
importe = leer_importe()
origen = input("Moneda de origen (EUR): ").strip().upper()
destino = input("Moneda de destino (USD): ").strip().upper()
try:
datos = convertir_moneda(importe, origen, destino)
except requests.Timeout:
print("La consulta tardó demasiado")
return
except requests.ConnectionError:
print("No hay conexión con el servicio")
return
except requests.HTTPError as error:
print(f"El servicio devolvió un error: {error.response.status_code}")
return
except (ValueError, KeyError) as error:
print(f"Datos inválidos: {error}")
return
resultado = datos["resultado"]
print(
f"{importe:.2f} {datos['origen']} = "
f"{resultado:.2f} {datos['destino']}"
)
if datos["fecha"]:
print(f"Fecha de referencia: {datos['fecha']}")
if __name__ == "__main__":
main()
Calcular la tasa implícita
tasa = resultado / importe if importe else Decimal("0")
print(f"1 {origen} = {tasa:.6f} {destino}")
No calcules la tasa cuando el importe sea cero. La cantidad de decimales visible depende del uso: dos pueden ser suficientes para el resultado monetario, pero una tasa suele necesitar más.
Separar lógica, red e interfaz
Un diseño mantenible divide el proyecto:
conversor/
├── api.py
├── servicio.py
├── validacion.py
├── main.py
└── tests/
api.py consulta el servicio. servicio.py aplica reglas. validacion.py comprueba entradas. main.py conversa con el usuario. Esta separación permite probar el cálculo sin realizar solicitudes reales.
Usar caché
Si muchas personas consultan el mismo par, guarda la tasa durante un período corto:
from datetime import datetime, timedelta, timezone
cache = {}
def guardar_tasa(clave, valor):
cache[clave] = {
"valor": valor,
"expira": datetime.now(timezone.utc) + timedelta(minutes=30),
}
Antes de reutilizarla, confirma que no haya expirado. Una caché reduce llamadas y mantiene el programa funcional ante interrupciones breves, pero debes mostrar la fecha para no presentar un valor antiguo como actual.
Conversión sin conexión
Puedes guardar la última respuesta en JSON y usarla como respaldo:
import json
from pathlib import Path
RUTA_CACHE = Path("tasas_cache.json")
def guardar_cache(datos):
RUTA_CACHE.write_text(
json.dumps(datos, ensure_ascii=False, indent=2),
encoding="utf-8",
)
Al utilizar datos guardados, informa claramente que son tasas anteriores. No ocultes la falta de conexión.
Claves de API y seguridad
Algunos proveedores requieren una clave. Nunca la escribas directamente en el repositorio:
import os
api_key = os.getenv("EXCHANGE_API_KEY")
if not api_key:
raise RuntimeError("Falta EXCHANGE_API_KEY")
Usa variables de entorno, limita los permisos y rota la clave si se publica accidentalmente.
Probar sin depender de Internet
Simula la respuesta HTTP con un mock. Prueba un resultado correcto, timeout, código 500, JSON inválido, moneda ausente y origen igual a destino. También prueba importes con coma, valores negativos y números muy grandes.
Crear una interfaz gráfica
Cuando la versión de terminal funcione, puedes añadir Tkinter. La interfaz debe llamar a la misma función de conversión en lugar de duplicar el cálculo. Realiza la solicitud fuera del hilo principal para que la ventana no se congele y desactiva el botón mientras la consulta está en curso.
Limitaciones importantes
Las tasas de referencia no incluyen comisiones, margen comercial, impuestos ni diferencias entre compra y venta. Tampoco representan necesariamente cotizaciones en tiempo real. Muestra la fuente, la fecha y una advertencia cuando el resultado se utilice para planificación financiera.
Errores frecuentes
No hagas solicitudes sin timeout. No confíes ciegamente en el JSON. No uses una clave pública dentro del código. No redondees antes de terminar el cálculo y no confundas el símbolo con el código ISO. Evita consultar la API en cada pulsación del teclado y respeta los límites del proveedor.
Conclusión
Un conversor de monedas combina una operación sencilla con prácticas reales de software: validación, HTTP, JSON, precisión decimal, caché y manejo de fallos. Separa la lógica de la interfaz, utiliza una fuente confiable, conserva la fecha de referencia y comunica las limitaciones. Con esta base puedes crear una herramienta de terminal, una aplicación gráfica o un pequeño servicio web sin duplicar la lógica principal.






