Avoid FloatField (DBR009)
models.FloatField maps to the database’s binary floating-point type (double precision in PostgreSQL),
which PostgreSQL’s own documentation calls inexact: “storing and retrieving a value might show slight
discrepancies”. The same documentation recommends numeric — Django’s DecimalField — “for storing
monetary amounts and other quantities where exactness is required”.
The failure mode is quiet and late. A total drifts by a cent, or a lookup against a stored value stops matching the number that was written. By the time anyone notices, the column holds production rows and the correction costs a migration plus a data audit.
This rule flags every FloatField on a Django model, so that reaching for a float is a decision somebody
made rather than a default nobody questioned. Where the quantity genuinely is inexact, # noqa: DBR009
records that decision in one line.
Wrong:
from django.db import models
class Invoice(models.Model):
# An amount that must add up exactly
total = models.FloatField()
# A tax rate compared against stored values
tax_rate = models.FloatField()
Correct:
from django.db import models
class Invoice(models.Model):
total = models.DecimalField(max_digits=10, decimal_places=2)
tax_rate = models.DecimalField(max_digits=5, decimal_places=4)
A float that is inexact by nature is fine — say so and move on:
from django.db import models
class Measurement(models.Model):
# A sensor reading carries its own measurement error; decimal precision would be false precision
temperature = models.FloatField() # noqa: DBR009
latitude = models.FloatField() # noqa: DBR009
Rationale
Binary floating point cannot represent most decimal fractions exactly, so sums and equality comparisons are unreliable for anything that has to balance
Decimalarithmetic is slower and its objects are larger, but that only matters in large numeric workloads, which are rare in an ORM-backed application; the everyday case is amounts, prices, rates and percentagesThe genuinely legitimate float — a measurement, a ratio, a coordinate — is exactly what a per-line
noqais forMigrations are exempt, being generated and out of the developer’s hands
Scope
The rule reports declarations of actual database columns. It recognises models.FloatField(...),
django.db.models.FloatField(...), an aliased models module (from django.db import models as db_models)
and a bare FloatField(...) imported from django.db.models.
Left alone:
Serializer and form fields (
serializers.FloatField,forms.FloatField) — emitting afloatover aDecimalFieldcolumn is the normal JSON boundaryThe
output_fieldof an annotation, aggregate,CastorExpressionWrapper, and theoutput_fieldclass attribute of a custom aggregate or database function — these type a query expression, not a column, andDecimalFieldis not a substitute therePlain
floattype annotations, which carry no persistence semantics
The one output_field that is reported belongs to models.GeneratedField: it decides the type of a real
generated column.
from django.db import models
from django.db.models import Avg
from django.db.models.functions import Cast
class Report(models.Model):
# Reported: this creates a float column
score = models.FloatField()
# Reported: a generated column is still a column
weighted = models.GeneratedField(
expression=models.F("score") * 2,
output_field=models.FloatField(),
db_persist=True,
)
def averages(queryset):
# Not reported: output_field types the annotation, no column involved
return queryset.annotate(
average=Avg("score", output_field=models.FloatField()),
as_float=Cast("score", output_field=models.FloatField()),
)