136

EDITED:

How can I set a Django field's default to a function that gets evaluated each time a new model object gets created?

I want to do something like the following, except that in this code, the code gets evaluated once and sets the default to the same date for each model object created, rather than evaluating the code each time a model object gets created:

from datetime import datetime, timedelta

class MyModel(models.Model):
    # default to 1 day from now
    my_datetime = models.DateTimeField(default=datetime.now() + timedelta(days=1))

ORIGINAL:

I want to create a default value for a function parameter such that it is dynamic and gets called and set each time the function is called. How can I do that? e.g.,

from datetime import datetime

def mydatetime(date=datetime.now()):
    print date

mydatetime() 
mydatetime() # prints the same thing as the previous call; but I want it to be a newer value

Specifically, I want to do it in Django, e.g.,

from datetime import datetime, timedelta

class MyModel(models.Model):
    # default to 1 day from now
    my_datetime = models.DateTimeField(default=datetime.now() + timedelta(days=1))
Super Kai - Kazuya Ito
  • 22,221
  • 10
  • 124
  • 129
Rob Bednark
  • 25,981
  • 23
  • 80
  • 125

7 Answers7

176

The question is misguided. When creating a model field in Django, you are not defining a function, so function default values are irrelevant:

from datetime import datetime, timedelta
class MyModel(models.Model):
    # default to 1 day from now
    my_datetime = models.DateTimeField(default=datetime.now() + timedelta(days=1))

This last line is not defining a function; it is invoking a function to create a field in the class.

In this case datetime.now() + timedelta(days=1) will be evaluated once, and stored as the default value.

PRE Django 1.7

Django [lets you pass a callable as the default][1], and it will invoke it each time, just as you want:

from datetime import datetime, timedelta
class MyModel(models.Model):
    # default to 1 day from now
    my_datetime = models.DateTimeField(default=lambda: datetime.now() + timedelta(days=1))

Django 1.7+

Please note that since Django 1.7, usage of lambda as default value is not recommended (c.f. @stvnw comment). The proper way to do this is to declare a function before the field and use it as a callable in default_value named arg:

from datetime import datetime, timedelta

# default to 1 day from now
def get_default_my_datetime():
    return datetime.now() + timedelta(days=1)

class MyModel(models.Model):
    my_date = models.DateTimeField(default=get_default_my_date)

More information in the @simanas answer below [1]: https://docs.djangoproject.com/en/dev/ref/models/fields/#default

Super Kai - Kazuya Ito
  • 22,221
  • 10
  • 124
  • 129
Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662
  • 3
    Thanks @NedBatchelder. This is exactly what I was looking for. I didn't realize that 'default' could take a callable. And now I see how my question was indeed misguided regarding function invocation vs function definition. – Rob Bednark Sep 30 '12 at 15:54
  • 34
    Note that for Django>=1.7 the use of lambdas is not recommended for field options because they are incompatible with migrations. Ref: [Docs](https://docs.djangoproject.com/en/1.7/ref/models/fields/#default), [ticket](https://code.djangoproject.com/ticket/22351) – StvnW Nov 28 '14 at 22:05
  • 8
    Please uncheck this answer, as it is wrong for Djange>=1.7. The answer by @Simanas is absolutely correct and should therefore be accepted. – AplusKminus Mar 30 '15 at 15:51
  • 1
    How does this work with migrations? Do all old objects get a date based on the time of migration? Is it possible to set a different default to be used with migrations? – timthelion Sep 07 '18 at 18:57
  • 12
    What if I want to pass some parameter to `get_default_my_date`? – Sadan A. Oct 18 '18 at 10:29
  • My migration looks like `('secret_key', models.CharField(default=core.models.models.getsecretkey, help_text='Hex-encoded secret key to generate totp tokens.', max_length=32, unique=True, validators=[django_otp.util.hex_validator])),` and throws the exception: `AttributeError: module 'django.db.models' has no attribute 'getsecretkey'`. Why is migrate looking for `getsecretkey()` in `django.db.models` instead of `core.models.models` per the migration? – Vishal May 03 '20 at 15:56
  • This doesn't work with migrations. See https://stackoverflow.com/questions/42496655/django-migration-default-value-callable-generates-identical-entry and https://docs.djangoproject.com/en/4.2/howto/writing-migrations/#migrations-that-add-unique-fields – Paul Schreiber Apr 05 '23 at 20:15
78

Doing this default=datetime.now()+timedelta(days=1) is absolutely wrong!

It gets evaluated when you start your instance of django. If you are under apache it will probably work, because on some configurations apache revokes your django application on every request, but still you can find you self some day looking through out your code and trying to figure out why this get calculated not as you expect.

The right way of doing this is to pass a callable object to default argument. It can be a datetime.today function or your custom function. Then it gets evaluated every time you request a new default value.

def get_deadline():
    return datetime.today() + timedelta(days=20)

class Bill(models.Model):
    name = models.CharField(max_length=50)
    customer = models.ForeignKey(User, related_name='bills')
    date = models.DateField(default=datetime.today)
    deadline = models.DateField(default=get_deadline)
Simanas
  • 2,793
  • 22
  • 20
  • 30
    If my default is based on the value of another field in the model, is it possible to pass that field as a parameter as in something like `get_deadline(my_parameter)`? – YPCrumble Mar 30 '15 at 22:01
  • @YPCrumble this should be a new question! – jsmedmar May 03 '16 at 20:19
  • 3
    Nope. That's not possible. You will have to use some different approach to do it. – Simanas May 04 '16 at 07:39
  • How do you remove the `deadline` field again while also deleting the `get_deadline` function? I have removed a field with a function for a default value, but now Django crashes on startup after removing the function. I could manually edit the migration, which would be ok in this case, but what if you simply changed the default function, and wanted to remove the old function? – beruic Nov 09 '16 at 13:50
  • `default=datetime.today` is not working for DJango version 3.2, My database is MySQL and running your code still setting default value to None. – Pankaj Sep 21 '21 at 12:17
8

There's an important distinction between the following two DateTimeField constructors:

my_datetime = models.DateTimeField(auto_now=True)
my_datetime = models.DateTimeField(auto_now_add=True)

If you use auto_now_add=True in the constructor, the datetime referenced by my_date is "immutable" (only set once when the row is inserted to the table).

With auto_now=True, however, the datetime value will be updated every time the object is saved.

This was definitely a gotcha for me at one point. For reference, the docs are here:

https://docs.djangoproject.com/en/dev/ref/models/fields/#datetimefield

Super Kai - Kazuya Ito
  • 22,221
  • 10
  • 124
  • 129
damzam
  • 1,921
  • 15
  • 18
5

Sometimes you may need to access model data after creating a new user model.

Here is how I generate a token for each new user profile using the first 4 characters of their username:

from django.dispatch import receiver
class Profile(models.Model):
    auth_token = models.CharField(max_length=13, default=None, null=True, blank=True)


@receiver(post_save, sender=User) # this is called after a User model is saved.
def create_user_profile(sender, instance, created, **kwargs):
    if created: # only run the following if the profile is new
        new_profile = Profile.objects.create(user=instance)
        new_profile.create_auth_token()
        new_profile.save()

def create_auth_token(self):
    import random, string
    auth = self.user.username[:4] # get first 4 characters in user name
    self.auth_token =  auth + ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits + string.ascii_lowercase) for _ in range(random.randint(3, 5)))
GalacticRaph
  • 872
  • 8
  • 11
3

You can't do that directly; the default value is evaluated when the function definition is evaluated. But there are two ways around it.

First, you can create (and then call) a new function each time.

Or, more simply, just use a special value to mark the default. For example:

from datetime import datetime

def mydatetime(date=None):
    if datetime is None:
        datetime = datetime.now()
    print datetime

If None is a perfectly reasonable parameter value, and there's no other reasonable value you could use in its place, you can just create a new value that's definitely outside the domain of your function:

from datetime import datetime

class _MyDateTimeDummyDefault(object):
    pass

def mydatetime(date=_MyDateDummyTimeDefault):
    if datetime is _MyDateDummyTimeDefault:
        datetime = datetime.now()
    print datetime

del _MyDateDummyTimeDefault

In some rare cases, you're writing meta-code that really does need to be able to take absolutely anything, even, say, mydate.func_defaults[0]. In that case, you have to do something like this:

def mydatetime(*args, **kw):
    if 'datetime' in kw:
        datetime = kw['datetime']
    elif len(args):
        datetime = args[0]
    else:
        datetime = datetime.now()
    print datetime
Super Kai - Kazuya Ito
  • 22,221
  • 10
  • 124
  • 129
abarnert
  • 354,177
  • 51
  • 601
  • 671
  • 1
    Note that there's no reason to actually instantiate the dummy value class - just use the class itself as the dummy value. – Amber Sep 29 '12 at 03:34
  • You CAN do this directly, just pass in the function instead of the result of the function call. See my posted answer. –  Sep 29 '12 at 04:45
  • No you can't. The result of the function isn't the same type of thing as the normal parameters, so it can't reasonably be used as a default value for those normal parameters. – abarnert Sep 29 '12 at 04:54
  • 1
    Well yes, if you pass in an object instead of a function it will raise an exception. The same is true if you pass a function into a parameter expecting a string. I don't see how that's relevant. –  Sep 29 '12 at 04:56
  • It's relevant because his `myfunc` function is made to take (and print) a `datetime`; you've changed it so it can't be used that way. – abarnert Sep 29 '12 at 04:57
  • I wasn't aware we weren't allowed to change the way the function is used. My way works just fine, it just requires you to pass a function instead of a date, which is just as (or more) elegant than taking a placeholder value. –  Sep 29 '12 at 05:00
  • It's not even close to as elegant. My solution leaves the API as it is; your replaces it with something unusual and unpythonic. That's why you see code like mine all over the standard library, and code like yours nowhere. – abarnert Sep 29 '12 at 05:02
1

Pass the function in as a parameter instead of passing in the result of the function call.

That is, instead of this:

def myfunc(datetime=datetime.now()):
    print datetime

Try this:

def myfunc(datetime=datetime.now):
    print datetime()
Super Kai - Kazuya Ito
  • 22,221
  • 10
  • 124
  • 129
  • This doesn't work, because now the user can't actually pass a parameter—you'll try to call it as a function and throw an exception. In which case you might as well just take no params and `print datetime.now()` unconditionally. – abarnert Sep 29 '12 at 04:50
  • Try it. You're not passing a date, you're passing the datetime.now function. –  Sep 29 '12 at 04:53
  • Yes, the default is passing the `datetime.now` function. But any user who passes a non-default value will be passing a datetime, not a function. `foo = datetime.now(); myfunc(foo)` will raise `TypeError: 'datetime.datetime' object is not callable`. If I actually wanted to use your function, I'd have to do something like `myfunc(lambda: foo)` instead, which is not a reasonable interface. – abarnert Sep 29 '12 at 04:55
  • I don't think its unreasonable to have an interface which requires a passed in function. The user would have to pass in a date-generating thing, not a date. So lambda: datetime.now() + timedelta(days=1), for the example. –  Sep 29 '12 at 04:59
  • I don't even know what to say to that. Nobody's going to expect an interface like that, or like it, or remember how to use it. It's just silly. – abarnert Sep 29 '12 at 05:01
  • 1
    Interfaces which accept functions are not unusual. I could start naming examples from the python stdlib, if you like. –  Sep 29 '12 at 05:02
  • Yes, higher-order functions take functions. Date functions take dates. `time.strftime`, for example, does not take a function that returns a time, nor does `str.__add__` take a function that returns a string. – abarnert Sep 29 '12 at 05:04
  • The question being asked here was to generate a value to populate a date field, not a "date function". I would agree, if you were writing a date library, but that's not this. –  Sep 29 '12 at 05:06
  • He's clearly trying to write an everyday, ordinary function with an everyday, ordinary interface, except that he wants the default to be something dynamic. You've replaced it with something different that can't be used the same way. Do you really think the `models.DateTimeField` interface expects (or should expect) a function that returns a date, instead of a date? – abarnert Sep 29 '12 at 05:11
  • No, I don't think that. I don't really care what the actual default DateTimeField behavior is, you'll notice neither of our solutions would work easily there, since we both need to change the behavior of that object (your solution to interpret the placeholder value, mine to accept a function.) And if you're going to be overriding that behavior anyway, then you can make your interface expect whatever you'd like. –  Sep 29 '12 at 05:15
  • 2
    Django already accepts a callable exactly as this question suggests. – Ned Batchelder Sep 29 '12 at 17:48
0

You should set get_my_datetime without () which returns the current date and time (+1 day) to DateTimeField() as a default value as shown below. *Don't set get_my_datetime() with () because the default date and time become when the Django server starts (Unchanged) and you should use Django's timezone.now() because Python's datetime.now() cannot save UTC correctly in database when TIME_ZONE = 'UTC' which is default in settings.py:

from datetime import timedelta
from django.utils import timezone

def get_my_datetime(): # ↓ Don't use "datetime.now()"
    return timezone.now() + timedelta(days=1)

class MyModel(models.Model):
    # default to 1 day from now
    my_datetime = models.DateTimeField(default=get_my_date)             
                        # Don't set "get_my_datetime()" ↑

And as your code, don't set datetime.now() + timedelta(days=1) because the default date and time become when the Django server starts (Unchanged):

from datetime import datetime, timedelta

class MyModel(models.Model):
    # default to 1 day from now              # ↓ ↓ ↓ ↓ ↓ ↓ Don't set ↓ ↓ ↓ ↓ ↓ ↓
    my_datetime = models.DateTimeField(default=datetime.now() + timedelta(days=1))
Super Kai - Kazuya Ito
  • 22,221
  • 10
  • 124
  • 129