The 'contribute_to_class' method

Robert Crowther Jan 2022
Last Modified: Feb 2023

The contribute_to_class() method seems to hang in midair in unrelated pieces of code. The key to the apparent unrelation of the code is that the method is called from elsewhere. It’s called from django.db.models.base.py in __new__().

The problem

The method contribute_to_class() answers a problem. The problem is that Django does a lot of class‐based setup. When I say class‐based, I do not mean with instances. Django configures much behaviour using class methods and attributes (where the class is a singular template for later object creation). For example, fields can be declared on model classes. This leads to a clean syntax, but causes setup problems. Django is already using ___new__() and metaclasses to set up the class. What if one of these attributes or methods need to ‘know’ about other parts of the class? The usual methods of constructing a specialised class are exhausted. If this had been the creation of an instance using __init__(), you would have a clean override and the new instance passed. Enter contribute_to_class().

The upshot

This method has a signature,

def contribute_to_class(self, model, name):

The method must be placed inside the value assigned to a method on a model (that could be a field, or any other Model attribute).

If you do that, the method runs when the class is built, not an instance. It lets you either gather information from the model, or place information from the attribute into the model.

If that’s not enough, next is a longer explanation…

In Model __new__()

Assume a Model class with declarations of fields. In Model __new__(), these fields are passed as attributes to be built into the new class. But Django code peels off attributes with a contribute_to_class() method,

contributable_attrs = {}
for obj_name, obj in list(attrs.items()):
    if _has_contribute_to_class(obj):
        contributable_attrs[obj_name] = obj
    else:
        new_attrs[obj_name] = obj

In some programming universes this is pretty shocking, perhaps ugly. It’s structural query by reflection—regarded as not a good way to go, but easy enough in Python’s object‐based universe. Anyway, Django digs in and finds attribute values (future field values) with the method. By the way, when Django looks in the ‘value’, the value must be not a class, but an instance of a class,

def _has_contribute_to_class(value):
    # Only call contribute_to_class() if it's bound.
    return not inspect.isclass(value) and hasattr(value, 'contribute_to_class')

Having stored all these special attribute values, the code in __new__() creates the new class. Then does a few tweaks, mostly with app labels, then gets back to the list of contribute_to_class() attributes,

# Add remaining attributes (those with a contribute_to_class() method)
# to the class.
for obj_name, obj in contributable_attrs.items():
    new_class.add_to_class(obj_name, obj)

And what does add_to_class() do?

def add_to_class(cls, name, value):
    if _has_contribute_to_class(value):
        value.contribute_to_class(cls, name)
    else:
        setattr(cls, name, value)

If the future attribute, as configured, has a contribute_to_class() method, the method is called. And nothing else happens. This is contrary to the code comment in __new__(), which may have been the case when the code was first written? Other values are set on the class as attributes—which is what would happen usually.

And why is the method broken out? Nowadays, other code can use it—add_to_class() is an established setter.

What use is contribute_to_class()?

Here’s the signature of contribute_to_class(),

def contribute_to_class(self, model, name):
    ...

‘name’ is the name of the attribute.

If you’ve followed up until now, we have an ‘attribute’ with a value. This value is an instance of a class. When the parent class is constructed, the contribute_to_class() method is called. The method is supplied with the now part‐constructed Model. That’s the point. This is what is missing from these __new__() class constructors, the ability customise setup using the parent class.

As the method name suggests, the attribute ‘value’ can put data into the Model. The general form is this,

class ModelFieldMax():
    def __init__(self, maxVal):
        self.maxVal = maxVal

    def contribute_to_class(self, model, name):
        setattr(model, 'maxVal', self.maxVal)



class SomeModel(models.Model):
    maxInput = SomeModelField(1.2)

On class creation, you can reach SomeModel.maxVal. Note that if you want SomeModel.maxInput.maxVal to exist too the attribute must be set explicitly. Normal __new__() class‐building has been subverted.

That was handy, but does it explain why this extra, and very distracted, code? Let’s say you have a field declared on a model. How would you get that data into a attribute called ‘_mets’, which is where Django likes to hold ‘information on a class’? You could override __new__() and go hunting for the data. Or you could use contribute_to_class(). That’s how db.models shoves all the information on classes onto the ‘_meta’ attribute—field lists and so forth. That’s what model.fields.files.py does to get descriptors into a Model class.

Self and contribute_to_class()

There’s another possibility here. Counter to it’s name, the method contribute_to_class() can also gather data from the class. Have you ever wondered how a model Manager gets the class of the Model it is attached to? Some pseudo‐code for how you might want to write this down,

class SomeModel(models.Model):
objects = Manager(SomeModel)

But Python can’t handle that. At the time the Manager is initialised, the model is not complete.

As it happens, there’s another part of Django can handle that. The app registry can lookup model names from strings, so you could try,

class SomeModel(models.Model):
    objects = Manager('SomeModel')

Then do a lookup in the Manager __init__() method for the class itself.

But that is not what happens. The Manager has a contribute_to_class() method, and this sets up the Manager with a reference to the Model it will be operating on. It can be declared like this (not far from the Django code, as it happens),

class Manager():
    def __init__(self):
        self.model = None

    def contribute_to_class(self, model, name):
        self.model = model



class SomeModel(models.Model):
    objects = Manager()

Models.__new__() will find the contribute_to_class() method, call it with the newly constructed class, and then the Manager will know. Which is not explicit, but is I guess clean or tidy, words a lot of Python/Django people like to use.

You may feel now that contribute_to_class() is a slightly unfortunate name. Probably historical. In the case above, the Manager is not contributing anything to the surrounding Model—it’s recording a reference to the Model for it’s own purposes.

Oh, and that’s how fields.related gathers info on a Model to get related information.

Summary

So, contribute_to_class()

The method is essential for Django to be able to use class declarations without the already complex __new__() method becoming a forest of ugly, overridden code.