So I've seen similar questions about this error, and they all seem to be related to use cases where there are ManyToMany relationships. However, I'm getting this issue even though my model doesn't have M2M relationships, so I wanted to ask and see why this was happening.
Here is my model:
class Course(models.Model):
name = models.CharField(max_length=64)
credit = models.IntegerField
notes = models.CharField(max_length=128)
resources = models.TextField
description = models.TextField
topic = models.CharField(max_length=128)
Whenever I create a new instance of this model, I get the TypeError
for the credit
, resources
, and description
fields.
I instantiate it like so:
c = Course(
name='some name',
credit='8',
notes='N/A',
resources='no resources',
description='N/A',
topic='some topic'
)
However if I change the affected fields to models.IntegerField(max_length=8)
and models.TextField(max_length=8)
, then the error goes away.
Why does this happen? I was under the impression that the max_length
parameter was optional for TextField
; I don't even know what it means in the context of an IntegerField
. Can someone explain this behavior, and/or what I'm doing wrong?
You are defining the fields as their classes' references, and not as an instance of those classes. It should be
class Course(models.Model):
name = models.CharField(max_length=64)
credit = models.IntegerField()
notes = models.CharField(max_length=128)
resources = models.TextField()
description = models.TextField()
topic = models.CharField(max_length=128)
If anyone's interested I think what happens in the django.db.models.ModelBase
metaclass is fairly interesting. Overly verbose for answering this question? Almost certainly, I was originally not going to post but perhaps it will help (assuming what I have is correct of course). I've always found it more useful to understand why something works rather than why it doesn't. I'm not going to attempt to try to explain every little part that would be a monumental task, and am sticking to what's (hopefully) directly relevant. ¯\_(ツ)_/¯
(Also, it's likely I made a few errors in places, I don't dive into source code very often, please do feel free to correct)
Let's create a random model:
class RandomModel(models.Model):
field1 = models.TextField()
Let's see what happens behind the scenes for something this deceptively simple. For the more complicated stuff, it's turtles all the way down.
We have models.Model
as the base class, but it has ModelBase
as it's metaclass. And, ModelBase
has a __new__
method:
The source for the __new__
method of this metaclass is fairly long. Here's a link to the contents of the file in django\db\models\base.py
.
There's two other classes we'll be concerned with django/db/models/options.py -> class Option
and django/db/models/fields/__init__.py -> class Field
.
Were you to print dir(field1)
or if you were to simply look inside __init__.py
in /fields
you would see the Field
class and a method named contribute_to_class
.
Remember the contribute_to_class
method, more on this later.
Let's break down the relevant components of the __new__
method. If you've ever seen a metaclass then you should be familiar with the arguments to the new function.
If not then check this link out: What is a metaclass in Python
class ModelBase(type):
def __new__(..., attrs):
....
attrs
here is the dictionary that contains all methods, attributes, etc.
Stuff happens, then we have this line:
new_class.add_to_class('_meta', Options(meta, app_label))
Now, what is add to class? It is this function:
def add_to_class(cls, name, value):
# We should call the contribute_to_class method only if it's bound
if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'):
value.contribute_to_class(cls, name)
else:
setattr(cls, name, value)
We could stop right here, and I could have actually started with this function. This is the entire problem. You never initialize models.FieldName and thus it is not an instance that passes the condition hasattr(value, 'contribute_to_class')
. Instead, the else statement is executed and it simply becomes a class attribute (which doesn't contribute). As the name suggests hasattr
simply checks if the object has said attribute available (passed as a string to hasattr).
If you just did print(field1)
with field1 = models.TextField
you'd see that it was simply a reference to the class django.db.models.fields.TextField
.
Getting back on track, let's keep going! What if it does pass this condition? Then, we have that add_to_class
now calls contribute_to_class
with the parameters cls
and name
.
But, what does contribute_to_class
do?!
Here's the function contribute_to_class
:
def contribute_to_class(self, cls, name, virtual_only=False):
self.set_attributes_from_name(name)
self.model = cls
if virtual_only:
cls._meta.add_field(self, virtual=True)
else:
cls._meta.add_field(self)
if self.choices:
setattr(cls, 'get_%s_display' % self.name,
curry(cls._get_FIELD_display, field=self))
Really, what we care about here is cls._meta.add_field(self)
.
Now, we need to backtrack. At this point things are probably starting to get confusing, but hopefully all will be clear in a moment.
Recall the add_to_class
line:
new_class.add_to_class('_meta', Options(meta, app_label))
Depending upon how long you've been using Django at some point it is very likely you've used the ._meta
attribute more than likely like this _meta.fields
.
Well, here it is!!
Now, recall (again) the add_to_class
function the Options
class instance has no method contribute_to_class
and is thus added to the class as an attribute _meta
.
I'm not going to paste the contribute_to_class
function here. It's also a bit long-winded and is fairly well commented. You can find it here and it's about 1/4 of the way down. I'd recommend reading the comments and once you have the function should be more or less intuitive.
So, now there's this new attribute _meta
(which is an Option class instance) that has the class method add_field
available to it.
And, here's the add_field
method:
def add_field(self, field, virtual=False):
# Insert the given field in the order in which it was created, using
# the "creation_counter" attribute of the field.
# Move many-to-many related fields from self.fields into
# self.many_to_many.
if virtual:
self.virtual_fields.append(field)
elif field.is_relation and field.many_to_many:
self.local_many_to_many.insert(bisect(self.local_many_to_many, field), field)
else:
self.local_fields.insert(bisect(self.local_fields, field), field)
self.setup_pk(field)
The comments are fairly self explanatory again. However, what might be confusing is the line bisect(list, thing_in_list)
. Here, bisect
is simply short for bisect.bisect_right from the python module bisect. As the comments says the fields are inserted in the order in which they are created, which is a sorted order. Using bisect allows the list to retain sorted order while inserting, it locates the proper insertion index.
What's next in the __new__
method of the ModelBase
metaclass?
We have the following line:
# Add all attributes to the class.
for obj_name, obj in attrs.items():
new_class.add_to_class(obj_name, obj)
Now, recall that attrs
is the dict containing well everything. So, it's looping through the object names (keys) and the objects (values) of this dictionary. And, as discussed earlier looking for instances with the contribute_to_class
method otherwise it is simply added as an attribute to the class instance (the new class to be created remember __new__
is actually creating the class here). And, that process sort of repeats.
Then, the next two lines:
new_fields = chain(
new_class._meta.local_fields,
new_class._meta.local_many_to_many,
new_class._meta.virtual_fields
)
field_names = {f.name for f in new_fields}
Here, itertools.chain is flatting these lists into a single list and returns a chain object.
This chain object is then iterated over calling the attribute name
from the Field
class (remember the fields are actually instances of (subclasses) Field
) into a set
and a set
is simply a collection of objects without duplicates.
More things happen, but I'm just going to end here as I think everything else is fairly redundant at this point for this example.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With