Bug
Numeric.result_processor computes the scale for the float-to-Decimal conversion inline, bypassing the decimal_return_scale parameter entirely. The sibling class Float.result_processor already uses self._effective_decimal_return_scale correctly; Numeric does not.
The bug only surfaces when the DBAPI does not support native decimals (i.e. dialect.supports_native_decimal is False), which is the case for sqlite3 and several other backends. In that path Numeric.result_processor runs:
# lib/sqlalchemy/sql/sqltypes.py – Numeric.result_processor (current)
return processors.to_decimal_processor_factory(
decimal.Decimal,
self.scale if self.scale is not None else self._default_decimal_return_scale,
)
self._effective_decimal_return_scale (defined in NumericCommon) checks self.decimal_return_scale first, then falls back to self.scale, then _default_decimal_return_scale. The inline expression in Numeric skips the first step entirely, so decimal_return_scale is silently ignored.
Reproducer (sqlite3, no extra dependencies)
The script below adds a Float(asdecimal=True) column alongside Numeric to contrast the two behaviors. Both are given decimal_return_scale=5 and the same input value; only Float honours the parameter.
import decimal
import sqlalchemy as sa
engine = sa.create_engine("sqlite://")
metadata = sa.MetaData()
t = sa.Table(
"t",
metadata,
sa.Column("numeric_val", sa.Numeric(10, 2, decimal_return_scale=5)),
sa.Column("float_val", sa.Float(decimal_return_scale=5, asdecimal=True)),
)
metadata.create_all(engine)
value = decimal.Decimal("1.23456")
with engine.begin() as conn:
conn.execute(t.insert(), {"numeric_val": value, "float_val": value})
numeric_result, float_result = conn.execute(
sa.select(t.c.numeric_val, t.c.float_val)
).one()
print(f"Numeric(10, 2, decimal_return_scale=5): {numeric_result!r}")
print(f"Float(decimal_return_scale=5, asdecimal=True): {float_result!r}")
print(f"Expected (both): Decimal('1.23456')")
Output (current)
Numeric(10, 2, decimal_return_scale=5): Decimal('1.23')
Float(decimal_return_scale=5, asdecimal=True): Decimal('1.23456')
Expected (both): Decimal('1.23456')
Float returns the correct value; Numeric silently falls back to scale=2.
Root cause
In sqltypes.py line 657–663, Numeric.result_processor bypasses decimal_return_scale:
# Numeric – wrong
return processors.to_decimal_processor_factory(
decimal.Decimal,
self.scale if self.scale is not None else self._default_decimal_return_scale,
)
Float.result_processor (line 770–771) already does it correctly:
# Float – correct
return processors.to_decimal_processor_factory(
decimal.Decimal, self._effective_decimal_return_scale
)
The one-line fix is to apply the same pattern to Numeric:
return processors.to_decimal_processor_factory(
decimal.Decimal, self._effective_decimal_return_scale
)
Environment
- SQLAlchemy version: 2.1.0b4 (also present in 2.0.x)
- Python version: 3.x
- Database: SQLite / sqlite3 (any dialect with
supports_native_decimal = False)
Bug
Numeric.result_processorcomputes the scale for the float-to-Decimal conversion inline, bypassing thedecimal_return_scaleparameter entirely. The sibling classFloat.result_processoralready usesself._effective_decimal_return_scalecorrectly;Numericdoes not.The bug only surfaces when the DBAPI does not support native decimals (i.e.
dialect.supports_native_decimal is False), which is the case for sqlite3 and several other backends. In that pathNumeric.result_processorruns:self._effective_decimal_return_scale(defined inNumericCommon) checksself.decimal_return_scalefirst, then falls back toself.scale, then_default_decimal_return_scale. The inline expression inNumericskips the first step entirely, sodecimal_return_scaleis silently ignored.Reproducer (sqlite3, no extra dependencies)
The script below adds a
Float(asdecimal=True)column alongsideNumericto contrast the two behaviors. Both are givendecimal_return_scale=5and the same input value; onlyFloathonours the parameter.Output (current)
Floatreturns the correct value;Numericsilently falls back toscale=2.Root cause
In sqltypes.py line 657–663,
Numeric.result_processorbypassesdecimal_return_scale:Float.result_processor(line 770–771) already does it correctly:The one-line fix is to apply the same pattern to
Numeric:Environment
supports_native_decimal = False)