Convertidores de tipos personalizados con mssql-python

Por defecto, el controlador mssql-python convierte los tipos de datos de Microsoft SQL en tipos Python apropiados (véase Mapeos de tipos de datos). El sistema de convertidor de salida permite anular este comportamiento para tipos SQL específicos, permitiendo:

  • Transformaciones de tipo personalizadas (por ejemplo, de money a float en lugar de Decimal)
  • Integración con bibliotecas de terceros (por ejemplo, datos espaciales para objetos Shapely)
  • Formato específico de la empresa (por ejemplo, convertir fechas en formatos de cadena personalizados)

Registrar convertidores de salida

Úsalo add_output_converter() para registrar una función convertidora. La clave puede ser un tipo de Python que coincida con el type_code de cursor.description, o bien un código entero de tipo SQL de ODBC como mssql_python.SQL_DECIMAL:

import mssql_python
from datetime import datetime

def my_datetime_converter(value):
    """Convert datetime to a custom display string."""
    if value is None:
        return None
    return value.strftime("%B %d, %Y at %I:%M %p")

conn = mssql_python.connect(connection_string)
conn.add_output_converter(datetime, my_datetime_converter)

cursor = conn.cursor()
cursor.execute("SELECT CAST('2026-07-16T10:30:00' AS DATETIME2)")
print(cursor.fetchval())  # July 16, 2026 at 10:30 AM

Elige entre una clave de tipo Python y una clave de código tipo SQL

Las dos formas clave difieren en la precisión con la que seleccionan las columnas:

  • Una clave de tipo Python se aplica a cada tipo SQL que se corresponde a ese tipo Python. Registrar un convertidor para Decimal transforma las columnas decimal, numeric, money y smallmoney.

  • Una clave de código entero del tipo SQL coincide con el código de tipo ODBC que devuelve la columna, que es más específico, pero no siempre corresponde a un único tipo SQL. SQL_NUMERIC transforma solo columnas numéricas . SQL_DECIMAL Transforma las columnas decimal, dinero y smallmoney , porque las tres reportan ese mismo código.

Usa una clave entera cuando una clave tipo Python es más amplia de lo que quieres, o cuando portes código desde pyodbc, que usa claves enteras. Usa una clave de tipo Python cuando quieras que un convertidor cubra toda una familia de tipos SQL.

import mssql_python

def tag_decimal(value):
    return f"D:{value}"

conn.add_output_converter(mssql_python.SQL_DECIMAL, tag_decimal)

cursor.execute("""
    SELECT CAST(12.34 AS decimal(10,2)),
           CAST(56.78 AS numeric(10,2)),
           CAST(90.12 AS money)
""")
print(cursor.fetchone())  # ('D:12.34', Decimal('56.78'), 'D:90.1200')

Orden de resolución del convertidor

Para cada columna, el controlador selecciona como máximo un convertidor, en este orden:

  1. El convertidor registrado para el código de tipo SQL ODBC de entero de la columna.

  2. El convertidor registrado para el tipo Python en cursor.description.

  3. El convertidor se registró para SQL_WVARCHAR, pero solo cuando el tipo Python de la columna es str o bytes.

Firma de función convertidora

Al registrar un convertidor con una clave de tipo de Python, el tipo que pasas a add_output_converter() debe coincidir con uno de los siguientes tipos de Python. La columna Converter receives muestra lo que el controlador envía a tu función:

cursor.description type_code El convertidor recibe Tipos SQL de Microsoft
int int tinyint, smallint, , int, bigint
float float real, float
decimal.Decimal Decimal decimal, numeric, , money, smallmoney
str bytes (codificado en UTF-16LE) char, varchar, nchar, nvarchar, xml
datetime.datetime datetime datetime, datetime2, smalldatetime
datetime.date date date
bytes bytes varbinary, binary, , geography, geometry
bool bool bit

Note

Los convertidores de tipo cadena (clave str) reciben bytes sin procesar en codificación UTF-16LE, no cadenas de Python decodificadas. Todos los demás tipos reciben el objeto Python ya convertido.

from decimal import Decimal

def decimal_converter(value: Decimal | None) -> float | None:
    if value is None:
        return None
    # value is a Decimal object for numeric/money types
    return float(value)

Gestionar convertidores

Utiliza estos métodos para inspeccionar o eliminar convertidores ya registrados en una conexión.

Obtén el conversor existente

Recuperar la función de convertidor actualmente registrada para un tipo:

converter = conn.get_output_converter(str)
if converter:
    print(f"Converter registered: {converter}")
else:
    print("Using default conversion")

Quita un convertidor

Desregistra un convertidor para que el controlador vuelva a la conversión predeterminada para ese tipo:

conn.remove_output_converter(str)
# str columns now use default conversion

Borrar todos los convertidores

Restablece todos los convertidores registrados para usar los mapeos de tipos por defecto de Microsoft SQL:

conn.clear_output_converters()
# All types now use default conversion

Patrones comunes de convertidores

Estos ejemplos muestran las funciones convertidoras más necesarias.

Convertir VARCHAR en mayúsculas

Los convertidores de tipo cadena reciben bytes en bruto en la codificación UTF-16LE:

def uppercase_converter(value):
    if value is None:
        return None
    return value.decode('utf-16-le').upper()

conn.add_output_converter(str, uppercase_converter)

cursor = conn.cursor()
cursor.execute("SELECT 'hello world'")
print(cursor.fetchval())  # HELLO WORLD

Convertir money a float

Por defecto, el controlador devuelve los tipos DECIMAL/NUMÉRICO como decimal.Decimal. Convertir a float:

from decimal import Decimal

def money_to_float(value):
    if value is None:
        return None
    return float(value)

conn.add_output_converter(Decimal, money_to_float)

cursor = conn.cursor()
cursor.execute("SELECT CAST(19.99 AS MONEY)")
result = cursor.fetchval()
print(type(result))  # <class 'float'>

Analizar datos JSON

Microsoft SQL Server puede almacenar JSON como texto. Analizar automáticamente en objetos de Python:

import json

def json_converter(value):
    if value is None:
        return None
    text = value.decode('utf-16-le')
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        return text  # Return as string if not valid JSON

conn.add_output_converter(str, json_converter)

cursor = conn.cursor()
cursor.execute("SELECT '{\"name\": \"Widget\", \"price\": 19.99}'")
data = cursor.fetchval()
print(data['name'])  # Widget

Caution

Este ejemplo convierte TODOS los valores de cadena (nvarchar, varchar, xml). Se intenta analizar JSON en cada columna de cadenas. En la práctica, aplica el análisis sintáctico JSON de forma selectiva en lugar de como un convertidor general.

Formato personalizado de fecha y hora

Convertir la fecha y hora a formato de cadena ISO:

from datetime import datetime

def datetime_to_iso(value):
    if value is None:
        return None
    return value.isoformat()

conn.add_output_converter(datetime, datetime_to_iso)

cursor = conn.cursor()
cursor.execute("SELECT TOP 1 ModifiedDate FROM Production.Product")
print(cursor.fetchval())  # 2014-02-08T10:01:36.827000

Consideraciones de seguridad

Caution

Cuando registras un convertidor de salida, la función que proporcionas se ejecuta sobre cada valor de base de datos correspondiente.

  • Registra únicamente convertidores procedentes de código de confianza.
  • Nunca aceptes funciones de convertidor a partir de la entrada del usuario.
  • Los convertidores maliciosos podían ejecutar código arbitrario o filtrar datos.
# DANGEROUS - never do this
def unsafe_example(user_converter_code):
    converter_func = eval(user_converter_code)  # Security risk!
    conn.add_output_converter(str, converter_func)

Ejemplo: Integración de datos espaciales

Integra los datos espaciales con la biblioteca Shapely:

from shapely import wkb

def geometry_converter(value):
    """Convert WKB binary to Shapely geometry object."""
    if value is None:
        return None
    return wkb.loads(value)

conn.add_output_converter(bytes, geometry_converter)

# Query must use STAsBinary() to get standard WKB format
cursor = conn.cursor()
cursor.execute(
    "SELECT SpatialLocation.STAsBinary() FROM Person.Address "
    "WHERE SpatialLocation IS NOT NULL AND City = 'Seattle'"
)
for row in cursor:
    shape = row[0]
    print(f"Point: ({shape.x:.4f}, {shape.y:.4f})")

Note

Registrar un bytes convertidor afecta a TODAS las columnas binarias (varbinaria, geografía, geometría). Usa clear_output_converters() después de las consultas espaciales si también consultas datos binarios no espaciales.

Ejemplo: Formato de fila personalizado

Crea un convertidor de clases de datos:

from dataclasses import dataclass

@dataclass
class Product:
    id: int
    name: str
    price: float

def fetch_products_as_dataclass(cursor):
    """Convert rows to Product dataclass instances."""
    cursor.execute(
        "SELECT ProductID, Name, ListPrice FROM Production.Product "
        "WHERE ListPrice > 0"
    )
    results = []
    for row in cursor:
        results.append(Product(
            id=row.ProductID,
            name=row.Name,
            price=float(row.ListPrice)
        ))
    return results

products = fetch_products_as_dataclass(cursor)
for p in products[:5]:
    print(f"{p.name}: ${p.price:.2f}")

Alcance y vida útil del convertidor

  • De ámbito por conexión: el controlador registra los convertidores para cada conexión, no de forma global.
  • Persistente: Los conversores permanecen activos hasta que los quitas o cierras la conexión.
  • No heredadas: Las conexiones nuevas no heredan conversores de otras conexiones.
conn1 = mssql_python.connect(connection_string)
conn1.add_output_converter(str, uppercase_converter)

conn2 = mssql_python.connect(connection_string)
# conn2 does NOT have the converter registered

Consideraciones sobre el rendimiento

  • El controlador invoca los convertidores para cada valor del tipo registrado.
  • Mantén las funciones del convertidor eficientes.
  • Para consultas de alto volumen, considera si es necesaria la conversión.
  • Analice el rendimiento si los convertidores afectan a la capacidad de procesamiento.
import time

def slow_converter(value):
    time.sleep(1)  # Avoid time consuming routines like this - adds 1 second for each value processed!
    return str(value) if value else None