PYTHON / DJANGO
Models and field types
Declare Django models with the right field types and options, and know where each option acts: the database column, Python conversion, or validation.
What you will learn
- Pick field types by data shape: CharField vs TextField, Decimal vs Float
- Distinguish null (database column) from blank (validation and forms)
- Read a model's real field list and defaults through Model._meta.fields
- Trigger field validation deliberately with full_clean(); save() skips it
Understanding Models and field types
A Django model class is a declaration, not a container. Each class attribute you assign a field object to describes three separate things at once: the column type the database should get, the Python value the attribute should hold, and the validation and widget behaviour used when the value arrives from a form. That is why CharField demands max_length (a VARCHAR needs a size) while TextField does not, and why DecimalField demands max_digits and decimal_places while FloatField takes neither.
Field options split cleanly along the same lines. null, unique, db_index, primary_key and db_column shape the column; blank, choices, verbose_name, help_text and validators shape validation and forms; default sits in Python and is applied when you instantiate or save an unset field, not by the database. Knowing which side an option lives on answers most beginner questions: null=True lets the column store NULL, blank=True lets a form submit nothing, and they are independent because a form is not a database.
Django also adds an implicit primary key. If no field sets primary_key=True, it injects an auto-incrementing id whose class comes from the DEFAULT_AUTO_FIELD setting, and that field is created with blank=True so validation ignores it while it is still None. Choosing the narrowest type that fits your data (PositiveIntegerField over IntegerField, SlugField over CharField, DecimalField over FloatField for money) buys you database constraints and validators for free, because each field type ships its own validator list.
import django
from django.conf import settings
settings.configure(
INSTALLED_APPS=[],
DATABASES={},
DEFAULT_AUTO_FIELD="django.db.models.BigAutoField",
USE_TZ=True,
)
django.setup()
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=120)
slug = models.SlugField(unique=True)
pages = models.PositiveIntegerField(default=0)
price = models.DecimalField(max_digits=6, decimal_places=2)
published = models.DateField(null=True, blank=True)
summary = models.TextField(blank=True)
class Meta:
app_label = "catalog"
def __str__(self):
return f"{self.title} ({self.pages}p)"
for f in Book._meta.fields:
print(f"{f.name:<10} {type(f).__name__:<20} null={str(f.null):<5} blank={f.blank}")
print(Book(title="Dune", pages=412))A field declaration simultaneously defines a database column, a Python type conversion, and a set of validation rules, and each field option belongs to exactly one of those three layers.
Worked examples
choices, default, and the display label
Shows that choices store the raw value while the human label is fetched through the generated get_FIELD_display method, and that an off-list value only fails at validation time.
import django
from django.conf import settings
settings.configure(
INSTALLED_APPS=[],
DATABASES={},
DEFAULT_AUTO_FIELD="django.db.models.BigAutoField",
USE_TZ=True,
)
django.setup()
from django.core.exceptions import ValidationError
from django.db import models
class Ticket(models.Model):
OPEN = "open"
CLOSED = "closed"
STATUS_CHOICES = [(OPEN, "Open"), (CLOSED, "Closed")]
subject = models.CharField(max_length=40)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default=OPEN)
class Meta:
app_label = "helpdesk"
t = Ticket(subject="Printer on fire")
print(t.status, "->", t.get_status_display())
t.status = "urgent"
try:
t.full_clean(validate_unique=False)
except ValidationError as e:
print(e.message_dict)Example explained
Line 1default=OPEN is applied in Python at instantiation, so t.status is 'open' before any save.
Line 2get_status_display() is generated by the field because choices was passed; it maps the stored value to its label.
Line 3Assigning 'urgent' succeeds silently: choices is a validation rule, not a Python type restriction.
Line 4full_clean() runs the field's validate() step, which raises ValidationError keyed by field name in message_dict.
max_length is a validator, not a Python limit
Demonstrates that field options are only enforced when validation runs, and that validation also coerces raw values into the field's Python type.
import django
from django.conf import settings
settings.configure(
INSTALLED_APPS=[],
DATABASES={},
DEFAULT_AUTO_FIELD="django.db.models.BigAutoField",
USE_TZ=True,
)
django.setup()
from django.core.exceptions import ValidationError
from django.db import models
class Tag(models.Model):
label = models.CharField(max_length=5)
weight = models.IntegerField(null=True, blank=True)
class Meta:
app_label = "tagging"
t = Tag(label="overlong", weight="7")
print(t.label, t.weight)
try:
t.full_clean(validate_unique=False)
except ValidationError as e:
print(e.message_dict)
t.label = "short"
t.full_clean(validate_unique=False)
print(repr(t.weight))Example explained
Line 1The constructor stores whatever you pass: an 8-character label and the string '7' are both accepted.
Line 2full_clean() applies MaxLengthValidator, which the CharField added because max_length was given.
Line 3The successful full_clean() writes cleaned values back onto the instance, so weight becomes the int 7.
Line 4null=True on weight is what allows the column to be NULL; blank=True is what allows a form to leave it empty.
Important notes
default is evaluated in Python, so pass the callable (default=timezone.now) rather than calling it (default=timezone.now()), which would freeze the value at import time.
Django adds an implicit id primary key only when no field declares primary_key=True; once you declare your own, the automatic id disappears and any code relying on it breaks.
Common mistakes
Adding null=True to a CharField or TextField to make it optional, which creates two different empty states ('' and NULL) so filters like exclude(summary='') silently miss the NULL rows.
Assuming Model.objects.create() enforces max_length or choices; save() never calls full_clean(), and SQLite ignores VARCHAR limits, so invalid rows land in the table.
Using FloatField for prices, which stores binary floats and makes totals drift by fractions of a cent instead of rounding exactly like DecimalField.
Try it yourself
Change, predict, then run
Define an Event model with title (CharField, max_length=80), starts_at (DateTimeField), price (DecimalField with max_digits=6, decimal_places=2) and is_online (BooleanField defaulting to False). Instantiate one with a 100-character title and a price of '12.345', then print the message_dict from full_clean(validate_unique=False).
Open the Python workspaceCheck your understanding
A model has email = models.EmailField(max_length=50). You run Person.objects.create(email="not-an-email") against SQLite. What happens?
- The row is saved with the invalid value, because save() does not run field validation
- A ValidationError is raised, because EmailField validates on every assignment
- The database rejects the insert, because EmailField creates a CHECK constraint
- The value is stored as NULL, because it failed the field's type conversion
Show answer
EmailField's validators, including its email check and MaxLengthValidator, run inside full_clean(), which ModelForms call but save() does not. The ValidationError answer is tempting because forms in the admin do reject the value, but that is the form layer calling full_clean(), not the field policing assignment; at the database layer EmailField is just a VARCHAR with no constraint.