Edit

Handle decimal and money values

The mssql-python driver maps SQL Server decimal, numeric, and money data types to Python's decimal.Decimal for accurate financial and scientific calculations. SQL Server provides the following precise numeric types:

SQL Type Python Type Precision Use Case
decimal(p,s) decimal.Decimal 1-38 digits Exact calculations
numeric(p,s) decimal.Decimal 1-38 digits Same as decimal
money decimal.Decimal 19 digits, 4 decimal Currency
smallmoney decimal.Decimal 10 digits, 4 decimal Small currency
float float ~15 digits Approximate
real float ~7 digits Approximate

Python Decimal type

Receive decimal values

Decimal columns return Python decimal.Decimal objects:

from decimal import Decimal
import mssql_python

conn = mssql_python.connect(connection_string)
cursor = conn.cursor()

cursor.execute(
    "SELECT SubTotal, TaxAmt, TotalDue "
    "FROM Sales.SalesOrderHeader WHERE SalesOrderID = 43659"
)
row = cursor.fetchone()

print(type(row.SubTotal))  # <class 'decimal.Decimal'>
print(row.SubTotal)        # Decimal('20565.6206')
print(row.TaxAmt)          # Decimal('1971.5149')
print(row.TotalDue)        # Decimal('23153.2339')

Send decimal values

Pass Decimal objects for precise insertion:

from decimal import Decimal

cursor.execute("""
    CREATE TABLE #ProductDemo (
        Name NVARCHAR(50),
        Price DECIMAL(10,2),
        Cost DECIMAL(10,2)
    )
""")
cursor.execute("""
    INSERT INTO #ProductDemo (Name, Price, Cost)
    VALUES (%(name)s, %(price)s, %(cost)s)
""", {
    "name": "Widget",
    "price": Decimal("199.99"),
    "cost": Decimal("87.50")
})
conn.commit()

Avoid float for financial data

Float types have precision issues that make them unsuitable for financial calculations. Always use Decimal for accurate results:

from decimal import Decimal

# Bad: float has precision issues
price = 0.1 + 0.2  # 0.30000000000000004

# Good: Decimal is exact
price = Decimal("0.1") + Decimal("0.2")  # Decimal('0.3')

# Use string constructor for literal values
correct = Decimal("19.99")  # Exact
avoid = Decimal(19.99)      # Might introduce float imprecision

Money type

Work with money columns

Retrieve and update SQL Server money columns using Python Decimal objects:

from decimal import Decimal

# Create and populate a temp table with a money column
cursor.execute("""
    CREATE TABLE #Accounts (
        ID INT,
        AccountID NVARCHAR(20),
        Balance MONEY
    )
""")
cursor.execute(
    "INSERT INTO #Accounts (ID, AccountID, Balance) VALUES (1, %(acct)s, %(bal)s)",
    {"acct": "ACCT-001", "bal": Decimal("1234.5678")}
)
conn.commit()

cursor.execute("SELECT AccountID, Balance FROM #Accounts WHERE ID = 1")
row = cursor.fetchone()

print(row.Balance)         # Decimal('1234.5678')
print(type(row.Balance))   # <class 'decimal.Decimal'>

# Money arithmetic
cursor.execute("""
    UPDATE #Accounts 
    SET Balance = Balance + %(amount)s 
    WHERE ID = %(id)s
""", {"amount": Decimal("100.00"), "id": 1})
conn.commit()

Money formatting

Format decimal values as currency strings with symbols and comma separators:

from decimal import Decimal

def format_currency(value: Decimal, symbol: str = "$") -> str:
    """Format decimal as currency string."""
    return f"{symbol}{value:,.2f}"

cursor.execute("SELECT TotalDue FROM Sales.SalesOrderHeader")
for row in cursor:
    print(format_currency(row.TotalDue))  # $1,234.56

Decimal precision and scale

Understand precision and scale

  • Precision (p): Total number of digits
  • Scale (s): Number of digits after decimal point

For example, decimal(10, 2) can store values from -99999999.99 to 99999999.99, while decimal(5, 4) can store values from -9.9999 to 9.9999.

Specify precision in parameters

The mssql-python driver infers precision and scale from Decimal values automatically:

from decimal import Decimal

# Create a temp table for the measurement value
cursor.execute("CREATE TABLE #Measurements (Value DECIMAL(18,6))")

# The driver infers precision from the Decimal value
value = Decimal("123.456789")
cursor.execute("INSERT INTO #Measurements (Value) VALUES (%(v)s)", {"v": value})
conn.commit()

Control rounding

Use the quantize() method to round Decimal values to a specific scale:

from decimal import Decimal, ROUND_HALF_UP, ROUND_DOWN

price = Decimal("19.999")

# Round to 2 decimal places
rounded = price.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
print(rounded)  # Decimal('20.00')

# Round down (truncate)
truncated = price.quantize(Decimal("0.01"), rounding=ROUND_DOWN)
print(truncated)  # Decimal('19.99')

Financial calculations

Safe arithmetic

Implement financial calculations using Decimal with proper rounding:

from decimal import Decimal, ROUND_HALF_UP

def calculate_total(price: Decimal, quantity: int, tax_rate: Decimal) -> Decimal:
    """Calculate order total with tax."""
    subtotal = price * quantity
    tax = (subtotal * tax_rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    return subtotal + tax

cursor.execute("SELECT ListPrice FROM Production.Product WHERE ProductID = 1")
price = cursor.fetchval()

total = calculate_total(
    price=price,
    quantity=3,
    tax_rate=Decimal("0.0825")  # 8.25% tax
)

cursor.execute("""
    CREATE TABLE #OrderCalc (
        ProductID INT, Quantity INT, Total DECIMAL(10,2), Tax DECIMAL(10,2)
    )
""")
cursor.execute("""
    INSERT INTO #OrderCalc (ProductID, Quantity, Total, Tax)
    VALUES (%(prod)s, %(qty)s, %(total)s, %(tax)s)
""", {"prod": 1, "qty": 3, "total": total, "tax": total * Decimal("0.0825")})

Percentage calculations

Calculate percentage-based discounts and adjustments:

from decimal import Decimal

def calculate_discount(price: Decimal, discount_percent: Decimal) -> Decimal:
    """Calculate discounted price."""
    discount = (price * discount_percent / 100).quantize(Decimal("0.01"))
    return price - discount

original = Decimal("99.99")
discounted = calculate_discount(original, Decimal("15"))  # 15% off
print(f"Original: ${original}, After discount: ${discounted}")

Split amounts

Divide a total amount evenly among multiple parties, handling remainders:

from decimal import Decimal, ROUND_DOWN

def split_amount(total: Decimal, ways: int) -> list[Decimal]:
    """Split amount evenly, handling remainder."""
    per_person = (total / ways).quantize(Decimal("0.01"), rounding=ROUND_DOWN)
    remainder = total - (per_person * ways)
    
    amounts = [per_person] * ways
    # Add remainder to first person
    amounts[0] += remainder
    return amounts

total = Decimal("100.00")
split = split_amount(total, 3)
print(split)  # [Decimal('33.34'), Decimal('33.33'), Decimal('33.33')]
print(sum(split))  # Decimal('100.00') - always equals original

Aggregation operations

SUM and AVG with decimals

SQL Server aggregation functions return Decimal types for precision:

# SUM preserves decimal type
cursor.execute("SELECT SUM(TotalDue) AS TotalSales FROM Sales.SalesOrderHeader")
total_sales = cursor.fetchval()
print(type(total_sales))  # <class 'decimal.Decimal'>

# AVG might return more decimal places
cursor.execute("SELECT AVG(ListPrice) AS AvgPrice FROM Production.Product")
avg_price = cursor.fetchval()
# Round to desired precision
avg_price = avg_price.quantize(Decimal("0.01"))

Handle NULL in aggregations

SQL Server aggregation functions can return None when no rows match the query:

cursor.execute("SELECT SUM(TotalDue) FROM Sales.SalesOrderHeader WHERE CustomerID = 0")
total_discount = cursor.fetchval()

# SUM returns NULL if no rows match
if total_discount is None:
    total_discount = Decimal("0.00")

Output converters for decimal

Output converters use the Python type shown in cursor.description as the key, not the ODBC SQL type integer. Register the converter with decimal.Decimal so it works for decimal, numeric, money, and smallmoney columns, which all map to decimal.Decimal.

Convert to float (when precision isn't critical)

For compatibility with libraries that expect float values, define an output converter:

import mssql_python
from decimal import Decimal

def decimal_to_float(value):
    """Convert Decimal to float."""
    if value is None:
        return None
    return float(value)  # value is already a Decimal object

conn = mssql_python.connect(connection_string)

# Convert decimals to float for compatibility with libraries that expect float
conn.add_output_converter(Decimal, decimal_to_float)

cursor = conn.cursor()
cursor.execute("SELECT ListPrice FROM Production.Product WHERE ProductID = 1")
row = cursor.fetchone()
print(type(row.ListPrice))  # <class 'float'>

Custom money formatting converter

Define a custom output converter to automatically format Decimal values as currency strings:

from decimal import Decimal

def money_to_string(value):
    """Convert Decimal to formatted string."""
    if value is None:
        return None
    return f"${value:,.2f}"  # value is already a Decimal object

conn.add_output_converter(Decimal, money_to_string)

cursor = conn.cursor()
cursor.execute("CREATE TABLE #Accounts (Balance DECIMAL(19,4))")
cursor.execute(
    "INSERT INTO #Accounts (Balance) VALUES (%(bal)s)",
    {"bal": Decimal("1234.56")}
)
conn.commit()

cursor.execute("SELECT Balance FROM #Accounts")
for row in cursor:
    print(row.Balance)  # "$1,234.56"

Common patterns

Currency conversion

Convert monetary amounts between currencies using exchange rates:

from decimal import Decimal

def convert_currency(amount: Decimal, rate: Decimal) -> Decimal:
    """Convert currency at given exchange rate."""
    return (amount * rate).quantize(Decimal("0.01"))

usd_amount = Decimal("100.00")
eur_rate = Decimal("0.92")
eur_amount = convert_currency(usd_amount, eur_rate)
print(f"${usd_amount} = €{eur_amount}")

Balance updates with transactions

Use transactions with appropriate lock hints to ensure atomic fund transfers and prevent lost updates:

from decimal import Decimal

def transfer_funds(conn, from_account: int, to_account: int, amount: Decimal):
    """Transfer funds between accounts atomically."""
    cursor = conn.cursor()
    conn.autocommit = False
    
    try:
        balances = {}
        for account_id in sorted((from_account, to_account)):
            cursor.execute(
                "SELECT Balance FROM #Accounts WITH (UPDLOCK) WHERE ID = %(id)s",
                {"id": account_id}
            )
            row = cursor.fetchone()
            if row is None:
                raise ValueError(f"Account {account_id} not found")
            balances[account_id] = row.Balance
        
        if balances[from_account] < amount:
            raise ValueError("Insufficient funds")
        
        # Debit source
        cursor.execute("""
            UPDATE #Accounts SET Balance = Balance - %(amount)s
            WHERE ID = %(id)s
        """, {"amount": amount, "id": from_account})
        
        # Credit destination
        cursor.execute("""
            UPDATE #Accounts SET Balance = Balance + %(amount)s
            WHERE ID = %(id)s
        """, {"amount": amount, "id": to_account})
        
        conn.commit()
        
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.autocommit = True

# Set up demo accounts
cursor = conn.cursor()
cursor.execute("CREATE TABLE #Accounts (ID INT, Balance MONEY)")
cursor.executemany(
    "INSERT INTO #Accounts (ID, Balance) VALUES (%(id)s, %(bal)s)",
    [{"id": 1, "bal": Decimal("1000.00")}, {"id": 2, "bal": Decimal("500.00")}]
)
conn.commit()

# Usage
transfer_funds(conn, from_account=1, to_account=2, amount=Decimal("500.00"))

The transfer locks both rows up front, lowest ID first, by issuing a separate SELECT ... WITH (UPDLOCK) for each account in sorted order. UPDLOCK prevents the lost update pattern where two transactions read the same balance and apply updates based on stale data. Issuing the locks as separate statements in sorted order is what guarantees the acquisition order, preventing opposing transfers between the same two accounts from acquiring locks in the reverse order and deadlocking.

Bulk insert with decimals

Insert multiple rows with Decimal values using executemany():

from decimal import Decimal

cursor.execute("""
    CREATE TABLE #BulkProducts (
        Name NVARCHAR(50),
        Price DECIMAL(10,2),
        Cost DECIMAL(10,2)
    )
""")

products = [
    {"name": "Widget A", "price": Decimal("19.99"), "cost": Decimal("8.50")},
    {"name": "Widget B", "price": Decimal("29.99"), "cost": Decimal("12.75")},
    {"name": "Widget C", "price": Decimal("39.99"), "cost": Decimal("18.00")},
]

cursor.executemany("""
    INSERT INTO #BulkProducts (Name, Price, Cost)
    VALUES (%(name)s, %(price)s, %(cost)s)
""", products)
conn.commit()

Best practices

Always use Decimal for financial math

Never use float for financial calculations; always use the Decimal type for precision:

# Wrong: float precision issues
price = 0.10
quantity = 3
total = price * quantity  # 0.30000000000000004

# Correct: Decimal is exact
price = Decimal("0.10")
quantity = 3
total = price * quantity  # Decimal('0.30')

Use string constructors

Construct Decimal values from strings to avoid float imprecision:

# Good: exact value
amount = Decimal("123.45")

# Risky: might inherit float imprecision
amount = Decimal(123.45)  # Could be 123.4500000000000028...

Always round before display or storage

Round Decimal values to the appropriate scale before displaying or persisting to the database:

from decimal import Decimal, ROUND_HALF_UP

calculated = Decimal("123.456789")
display = calculated.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

Set decimal context if needed

The decimal context precision affects the results of calculations, not values parsed from strings or read from the database. A query always returns a money or decimal value with its full stored scale, regardless of getcontext().prec. The context only takes effect when you compute with it. To see the precision take effect, perform an operation such as division.

from decimal import Decimal, getcontext

# A money value read from SQL Server arrives at its full stored scale,
# regardless of the context precision.
cursor.execute(
    "SELECT SubTotal FROM Sales.SalesOrderHeader WHERE SalesOrderID = 43659"
)
subtotal = cursor.fetchone().SubTotal
print(subtotal)  # Decimal('20565.6206')

# The context precision governs the calculation, not the fetched value.
# Split the subtotal into three equal installments.
getcontext().prec = 10
print(subtotal / 3)  # 6855.206867 (10 significant digits)

getcontext().prec = 28  # Reset to default
print(subtotal / 3)  # 6855.206866666666666666666667 (28 significant digits)