Filter concepts by levelShowing all levels.

Django · Section 11

Django Field Types

Level
intermediate
Read
28 min
Concepts
3

The twenty common model field types, grouped by what they store — text and numeric (with DecimalField vs FloatField's real precision difference), date/time (auto_now vs auto_now_add, and why neither applies to a bulk update), and specialized/validated/file fields (EmailField/URLField/SlugField's built-in validators, JSONField's callable-default requirement, and ImageField's Pillow dependency).

This section

What is true here

  1. DecimalField stores an exact base-10 Decimal — the only correct choice for money; FloatField's binary float has real, compounding rounding error.
  2. auto_now_add sets a field once, at creation; auto_now updates it on every save() — both are Model.save() behavior, never applied by a bulk QuerySet.update().
  3. EmailField/URLField/SlugField are all CharField plus a built-in validator; SlugField also sets db_index=True automatically.
  4. JSONField's default must be a callable (dict, list) — a literal {} or [] would be shared across every instance, the same mutable-default trap as a Python function argument.
  5. ImageField extends FileField and requires the Pillow library to be installed separately.

What you will be able to do

  • Choose DecimalField over FloatField for any monetary or exact-arithmetic value
  • Use auto_now/auto_now_add correctly, and set an auto_now field explicitly during a bulk update
  • Choose the right validated field (EmailField/URLField/SlugField) instead of a plain CharField plus manual validation
  • Set a JSONField default correctly, and know ImageField's Pillow requirement

Text, numeric, and date/time fields

The everyday field types every model reaches for, and the two real gotchas among them — DecimalField vs FloatField, and auto_now vs auto_now_add.

Text and numeric field types

corebeginner

CharField requires max_length (a bounded string); TextField has none (unbounded). Integer/BigInteger/PositiveInteger differ only in range. DecimalField (max_digits + decimal_places, fixed precision) is for money; FloatField (native float, imprecise) is not.

Think of it as

DecimalField vs FloatField is the one distinction worth internalizing here: DecimalField stores an exact base-10 number (Python's Decimal), FloatField stores an approximate binary one (Python's float) — 0.1 + 0.2 genuinely does not equal 0.3 in binary floating point, a rounding error that compounds across thousands of transactions if used for money. Everything else in this group is a straightforward choice of range (Integer vs BigInteger) or shape (bounded vs unbounded text).

python
price = models.DecimalField(max_digits=10, decimal_places=2)
# max_digits=10, decimal_places=2 -> up to 99999999.99

What we're doing: See the real precision difference between DecimalField and FloatField for a value that exposes binary floating-point error.

shellpython
from decimal import Decimal

# FloatField-equivalent arithmetic
0.1 + 0.2   # 0.30000000000000004 — not exactly 0.3

# DecimalField-equivalent arithmetic
Decimal("0.1") + Decimal("0.2")   # Decimal('0.3') — exact
4
Binary floating point cannot represent 0.1 or 0.2 exactly — the tiny error is invisible in a single value but compounds across many additions, which is exactly what a running account balance or an invoice total does.
7
Decimal built from strings (not floats) represents the value exactly in base 10 — the same representation DecimalField stores in the database.

Why this works: This is not a Django-specific quirk — it is how IEEE 754 binary floating point works in every language that uses it. DecimalField exists specifically so a model field storing money never inherits this class of rounding error, by using a different underlying representation (base-10, arbitrary precision) instead of trying to work around float's limitations after the fact.

Using FloatField for a price or any other monetary value

Wrong

python
class Order(models.Model):
    total = models.FloatField()   # accumulates rounding error over many operations

Better

python
class Order(models.Model):
    total = models.DecimalField(max_digits=10, decimal_places=2)

What you see: A sum of line-item totals doesn't exactly equal the stored order total after enough transactions — off by a cent, in a way that is very hard to reproduce or explain from any single record.

Why: Every FloatField arithmetic operation risks the same binary-representation error as 0.1 + 0.2 — individually negligible, but compounding unpredictably across many additions, subtractions, and stored/reloaded values over an order's lifetime. DecimalField's exact base-10 representation has no such error to accumulate.

Exact base-10 vs. approximate binary

Decimal("0.1") + Decimal("0.2")

Decimal('0.3') — exact

0.1 + 0.2 (float)

0.30000000000000004 — not exact

  1. Decimal("0.1") + Decimal("0.2") — Decimal('0.3') — exact
  2. 0.1 + 0.2 (float) — 0.30000000000000004 — not exact

Text and numeric fields at a glance

Text and numeric fields at a glance
FieldPython typeNotable requirement/gotcha
CharFieldstrmax_length is required
TextFieldstrno length limit enforced
IntegerFieldint±2.1 billion range, all backends
BigIntegerFieldint±9.2 quintillion range
PositiveIntegerFieldint0 to ~2.1 billion — 0 itself is allowed
DecimalFieldDecimalmax_digits + decimal_places both required
FloatFieldfloatbinary floating point — imprecise, never for money
BooleanFieldbooldefault is None unless Field.default is set

Together

python
class Product(models.Model):
    name = models.CharField(max_length=200)
    description = models.TextField()
    stock = models.PositiveIntegerField(default=0)
    price = models.DecimalField(max_digits=10, decimal_places=2)

Remember: DecimalField (exact, max_digits+decimal_places) for money; FloatField (binary, imprecise) never for money. CharField requires max_length; TextField has none.

See also: models · primary keys and field options · date and time fields

Date and time field types

coreintermediate

DateField/DateTimeField/TimeField hold Python date/datetime/time objects; DurationField holds a timedelta. auto_now_add sets the value ONCE at creation; auto_now updates it on EVERY save() — both make the field non-editable and are only applied by Model.save(), never by QuerySet.update().

Think of it as

auto_now_add is a birth certificate — stamped once, permanently, at creation. auto_now is a "last modified" stamp — re-stamped every single time save() runs. The critical shared caveat: both are a behavior of Model.save() specifically, not of the database or the field itself — a bulk QuerySet.update() call bypasses save() entirely, so an auto_now field silently does NOT update from one of those.

python
class Task(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)
    duration = models.DurationField(null=True, blank=True)

What we're doing: Track both creation and last-modification timestamps on a model, using the two options together correctly.

articles/models.pypython
class Article(models.Model):
    title = models.CharField(max_length=200)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
3
created_at is set exactly once — the first time this row is ever saved — and is never touched again by any subsequent save().
4
updated_at is reset to the current time on EVERY save() call, including ones that only change title, giving an accurate "last touched" timestamp.

Why this works: Using both fields together, each with the option matching what it needs to track, is the standard pattern for "when was this created, and when was it last changed" — a single DateTimeField cannot answer both questions, since auto_now_add and auto_now are mutually exclusive behaviors on any one field.

Expecting auto_now to update during a bulk QuerySet.update()

Wrong

python
Article.objects.filter(category="news").update(is_published=True)
# updated_at (auto_now=True) is NOT touched by this call

Better

python
from django.utils import timezone

Article.objects.filter(category="news").update(
    is_published=True,
    updated_at=timezone.now(),   # set explicitly — update() bypasses auto_now
)

What you see: A bulk-updated row's "last modified" timestamp still shows an old value, even though the row genuinely changed — silently wrong data in anything relying on updated_at for freshness.

Why: auto_now (and auto_now_add) are implemented as behavior inside Model.save() — QuerySet.update() is a direct, bulk SQL UPDATE that never calls save() on any instance, so none of save()'s special field behavior runs. Any field that needs to change during a bulk update has to be set explicitly in the update() call itself.

auto_now_add vs auto_now

auto_now_add

  • +Set ONCE, at creation
  • +Never changes again
  • +A "birth certificate"

auto_now

  • Re-set on EVERY save()
  • A "last modified" stamp
  • Neither applies during QuerySet.update()
  • auto_now_add
    • Set ONCE, at creation
    • Never changes again
    • A "birth certificate"
  • auto_now
    • Re-set on EVERY save()
    • A "last modified" stamp
    • Neither applies during QuerySet.update()

Date/time fields at a glance

Date/time fields at a glance
FieldPython typeNotable option
DateFielddatetime.dateauto_now / auto_now_add available
DateTimeFielddatetime.datetimeauto_now / auto_now_add available; respects USE_TZ
TimeFielddatetime.timeauto_now / auto_now_add available
DurationFielddatetime.timedeltastored differently per backend — compare within Django, not raw SQL

Together

python
class Article(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)   # stamped once
    updated_at = models.DateTimeField(auto_now=True)       # re-stamped every save()

Remember: auto_now_add sets the value once, at creation; auto_now updates it on every save(). Neither applies during a bulk QuerySet.update() — only Model.save() triggers them.

See also: text and numeric fields · time zone support

Advertisement

Specialized, validated, and file fields

Fields that ship built-in validation, plus JSON and file storage.

Specialized, validated, and file fields

coreintermediate

EmailField/URLField/SlugField are CharField plus a built-in validator (and, for SlugField, an automatic db_index). GenericIPAddressField validates and normalizes IPv4/IPv6. JSONField stores JSON-serializable Python data — its default must be a callable (dict, list), never a literal {} or []. FileField/ImageField store a path under MEDIA_ROOT; ImageField additionally requires the Pillow library.

Think of it as

EmailField, URLField, and SlugField are all "CharField with opinions" — same underlying storage, but each ships a validator that rejects malformed input before it ever reaches the database. JSONField's mutable-default trap is the same Python gotcha as a function argument default={} — one dict object would otherwise be shared across every instance that didn't specify its own value, so Django requires the CALLABLE (dict, not {}) for the same reason a Python function needs default=None instead of default=[].

python
settings = models.JSONField(default=dict)   # callable, not default={}
tags = models.JSONField(default=list)       # callable, not default=[]

What we're doing: Store per-user preferences as JSON, with a correct callable default rather than a shared mutable literal.

accounts/models.pypython
class Profile(models.Model):
    user = models.OneToOneField("auth.User", on_delete=models.CASCADE)
    preferences = models.JSONField(default=dict)   # a fresh {} per instance
3
default=dict passes the dict TYPE itself (a callable) — Django calls it fresh for every new instance, so each Profile genuinely gets its own empty {}.

Why this works: JSONField's underlying storage is just a JSON-serializable Python value — the callable-default requirement exists purely to avoid the classic Python mutable-default-argument trap, where a single {} object created once (default={}) would otherwise be reused and shared as the "default" across every instance that never explicitly set its own value.

Using a literal mutable default on JSONField

Wrong

python
preferences = models.JSONField(default={})   # one shared dict object

Better

python
preferences = models.JSONField(default=dict)   # a fresh {} per instance

What you see: django.core.exceptions.FieldError raised at model-loading time — Django's own system checks explicitly reject a mutable default like {} or [] on JSONField before this can even reach production and cause the classic shared-object bug.

Why: default={} would otherwise mean every instance that doesn't specify its own value shares the exact same dict object as its "default" — mutating one instance's default in place could leak into every other instance's. Django's system checks catch this specific case and refuse to start rather than let the classic mutable-default bug reach runtime.

EmailField/URLField/SlugField are CharField plus a validator

CharField

raw string storage

+ a built-in validator

EmailValidator / URLValidator / slug rules

EmailField / URLField / SlugField

rejects malformed input before saving

  1. CharField — raw string storage
  2. + a built-in validator — EmailValidator / URLValidator / slug rules
  3. EmailField / URLField / SlugField — rejects malformed input before saving

Specialized, validated, and file fields

Specialized, validated, and file fields
FieldPython typeNotable behavior
UUIDFielduuid.UUIDsee primary-keys-and-field-options for default=uuid.uuid4
EmailFieldstrCharField + EmailValidator, max_length=254 default
URLFieldstrCharField + URLValidator, max_length=200 default
SlugFieldstrCharField + slug validator, db_index=True automatically, max_length=50 default
GenericIPAddressFieldstrvalidates/normalizes IPv4 and IPv6
JSONFielddict / list / str / int / bool / Nonedefault MUST be a callable, e.g. dict, not {}
FileFieldFieldFilepath relative to MEDIA_ROOT
ImageFieldFieldFilerequires Pillow; adds height_field/width_field

Together

python
class Profile(models.Model):
    email = models.EmailField()
    website = models.URLField(blank=True)
    slug = models.SlugField(unique=True)
    preferences = models.JSONField(default=dict)
    avatar = models.ImageField(upload_to="avatars/", blank=True)

Remember: JSONField's default must be a callable (dict, not {}); SlugField auto-indexes; EmailField/URLField are CharField plus a validator; ImageField needs Pillow installed.

See also: text and numeric fields · primary keys and field options · media files

Advertisement