dropbox / sqlalchemy-stubs

Mypy plugin and stubs for SQLAlchemy
Apache License 2.0
570 stars 101 forks source link

Data type for Float column should be float, not Decimal #178

Open KKawamura1 opened 3 years ago

KKawamura1 commented 3 years ago

Related to: https://github.com/dropbox/sqlalchemy-stubs/issues/131

About

https://github.com/dropbox/sqlalchemy-stubs/pull/132 fixed https://github.com/dropbox/sqlalchemy-stubs/issues/131, a bug that sqlalchemy.Numeric was treated as float, not decimal.Decimal. But it may introduce a new bug, that sqlalchemy.Float is also treated as decimal.Decimal, not float.

To reproduce

Assigning a float value to sqlalchemy.Float is enough to reproduce this bug.

Below is a sample code to reproduce:

from decimal import Decimal
from sqlalchemy import Column, Float, Numeric, Integer
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Numbers(Base):
    __tablename__ = "numbers"
    id_ = Column(Integer, primary_key=True)

    c_numeric = Column(Numeric, nullable=False)
    c_numeric_as_decimal = Column(Numeric(asdecimal=True), nullable=False)
    c_numeric_as_float = Column(Numeric(asdecimal=False), nullable=False)
    c_float = Column(Float, nullable=False)

def in_float() -> None:
    number = 1.0
    numbers = Numbers(c_numeric=number, c_numeric_as_decimal=number, c_numeric_as_float=number, c_float=number)
    print(type(numbers.c_numeric), type(numbers.c_numeric_as_decimal), type(numbers.c_numeric_as_float), type(numbers.c_float))

def in_decimal() -> None:
    number = Decimal(1.0)
    numbers = Numbers(c_numeric=number, c_numeric_as_decimal=number, c_numeric_as_float=number, c_float=number)
    print(type(numbers.c_numeric), type(numbers.c_numeric_as_decimal), type(numbers.c_numeric_as_float), type(numbers.c_float))

in_float()
in_decimal()
$ mypy foo.py
foo.py:21: error: Incompatible type for "c_numeric" of "Numbers" (got "float", expected "Decimal")
foo.py:21: error: Incompatible type for "c_numeric_as_decimal" of "Numbers" (got "float", expected "Decimal")
foo.py:21: error: Incompatible type for "c_numeric_as_float" of "Numbers" (got "float", expected "Decimal")
foo.py:21: error: Incompatible type for "c_float" of "Numbers" (got "float", expected "Decimal")
Found 4 errors in 1 file (checked 1 source file)

Expected result

$ mypy foo.py
foo.py:21: error: Incompatible type for "c_numeric" of "Numbers" (got "float", expected "Decimal")
foo.py:21: error: Incompatible type for "c_numeric_as_decimal" of "Numbers" (got "float", expected "Decimal")
foo.py:26: error: Incompatible type for "c_numeric_as_float" of "Numbers" (got "Decimal", expected "float")  # this one
foo.py:26: error: Incompatible type for "c_float" of "Numbers" (got "Decimal", expected "float")  # this one
Found 4 errors in 1 file (checked 1 source file)

Environment

$ (cd sqlalchemy-stubs && git rev-parse HEAD)
55470ceab8149db983411d5c094c9fe16343c58b
$ python -c "import sqlalchemy; print(sqlalchemy.__version__)"
1.3.20
$ python -V
Python 3.8.2
$ mypy -V
mypy 0.790
shawnwall commented 3 years ago

bumping this case, as this is also causing problems for me. thx @KKawamura1

jroberts07 commented 3 years ago

Also experiencing this

wlcx commented 3 years ago

A workaround to avoid resorting to any # type: ignores is to use typing.cast:

def func_that_uses_the_column() -> float:
    foo = db.query(Foo).first()
    return cast(float, foo.float_column)

https://mypy.readthedocs.io/en/stable/casts.html#casts-and-type-assertions

deanle17 commented 3 years ago

@wlcx I think the main problem is assigning float value to a Float column. In order to do that, I have to Decimal(my_float_value) to make mypy happy. But it also leads to problem that I'm injecting to database a Decimal value which is defined as Float

from sqlalchemy import Float
​
class Coordinate(Base):
    __tablename__ = 'coordinates'

    latitude = Column(Float) #sqlalchemy-stub infers this column as Decimal
    longitude = Column(Float)
​

coord = Coordinate(
    latitude=50.5, #mypy complains that this value need to be Decimal
    longitude=60.5,
)
​
from decimal import Decimal

coord_2= Coordinate(
    latitude=Decimal(50.5), #works fine
    longitude=Decimal(60.5),
)
mthuurne commented 2 years ago

This problem also occurs when using only Float columns:

from sqlalchemy import Column, Float, Integer
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class MyModel(Base):
    __tablename__ = "my_model"

    id = Column(Integer, primary_key=True)
    value = Column(Float, nullable=False)

def f(data: MyModel) -> None:
    reveal_type(data.value)

Expected output:

testcase.py:15: note: Revealed type is "builtins.float*"

Actual output:

testcase.py:15: note: Revealed type is "decimal.Decimal*"

I'm currently using the following workaround:

from typing import cast

from sqlalchemy import Float as Float_org
from sqlalchemy.sql.type_api import TypeEngine

Float = cast(type[TypeEngine[float]], Float_org)

But it would be nice if this could be fixed in the stubs.