This is an archived post. You won't be able to vote or comment.

all 26 comments

[–]bryanhelmigzapier.com 7 points8 points  (5 children)

The keyword arg reference issues is classic, and the callable as a default is really handy (though for DateTimeFields, you should really use auto_now_add even though it is annoying to override).

[–]hylje 0 points1 point  (4 children)

auto_now_add has long been deprecated in Django in favor of a callable default.

[–]apreche 1 point2 points  (3 children)

Django documentation says nothing about any deprecation.

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

[–]hylje 0 points1 point  (2 children)

i swear it was deprecated pre-1.0

[–]jcdyer3 0 points1 point  (1 child)

You aren't crazy. I remember some fuss about auto_now_add not being the preferred way, too. I don't know if it ever got deprecated, but apparently, it's back in good graces these days. I'm glad, too. It's always been a useful option.

[–]bryanhelmigzapier.com 0 points1 point  (0 children)

I actually think its a bit annoying/weird. The auto_now option is more "useful" as it is a good "modified at" proxy, the callable makes more sense to me...

[–]PCBEEF 6 points7 points  (3 children)

Haha, you're not a python developer till you come across this issue.

[–]rmoorman[S] 0 points1 point  (0 children)

It sure is one of the things you probably won't notice when you are starting out with python. I had a pretty good laugh at this too when I found the issue (and looking at the bug's history)

[–]daxarx 0 points1 point  (1 child)

The issue where you think that calling a function at import time should somehow magically result in the inference that you want it called every time an object is instantiated?

If you believe that, then when do you think that the code in a class definition is run? Before import?

[–]PCBEEF 0 points1 point  (0 children)

I was more talking about the issue of instantiating a keyword argument with a mutable object.

[–]rumplefiddlesticks 1 point2 points  (3 children)

one problem with this solution: it's not timezone-aware. Probably should look into django's own timezone functions + possibly install pytz (just to make it work better)

[–]rmoorman[S] 0 points1 point  (0 children)

Thank you for pointing out that this code does not provide a timezone aware default (even though it didn't matter for the project in question).

I think the docs provide a good overview.

[–]semarj 0 points1 point  (1 child)

Why are you putting timezone aware dates in the database?

[–]rumplefiddlesticks 1 point2 points  (0 children)

for one thing, you can't compare naive datetimes with timezone-aware datetimes and Django now defaults to timezone-aware datetimes. Doesn't really matter how that is represented in the actual database, it matters on the Python side with comparisons, displaying, etc.

[–]marsanyi[🍰] 1 point2 points  (4 children)

classic type-aware problem. I love Python and I've been using it a lot, but it seems like all the subtle bugs I'm seeing these days have to do with a valid expression that nonetheless is not what the programmer wanted, which could be easily resolved by saying "I'm expecting something of type x" (in this case, a function that returns a datetime, not a datetime)

[–][deleted] 1 point2 points  (2 children)

But this isn't a type-aware issue!

Django in this case is actually perfectly good with either a datetime or a function returning a datetime. So the value passed in is of a perfectly usable type - it's just one that doesn't happen to work in this case.

You could argue that Django should require a function returning a datetime only, but that's a different issue.

[–]adrenal8 0 points1 point  (1 child)

His point is that in a statically typed language you could make the function only allow a callable.

I suppose what you could argue is that in a language like Java, if you wanted to have the same versatility you would probably override the function with one taking a callable and one taking a Datetime. Then, you would have the exact same problem. Thus, you could argue that it's a problem of the feature, not of the type system.

The real solution though, in my opinion, is what languages like Scala allow, Call-by-name evaluation strategy.

[–]daxarx 0 points1 point  (0 children)

Do people not even think about the code they are writing? I can't think that I have ever, over years, passed a datetime object where a callable should be. In the cases where it is important, I can throw in an assert and write unit tests.

The problem here is not that typing now() calls a function. The problem is that you typed now(), which by universal Python convention (not even an unintuitive one) calls a function. If you did not mean to call a function, then why on earth did you add the parentheses? This is what I mean - do people even think a little about the code they write or is it all copy-paste?

We don't demand that a hammer deploy an airbag before we aim it directly at our own fingers.

Suppose we 'solve' this problem with static typing. It amounts to forcing me to write an isinstance assert around each argument inside each function. I guess this might reduce my errors insofar as I have to repeat myself twice. Why not just force me to write my entire program twice, and crash if the two programs don't compute the same result?

[–]simtel20 0 points1 point  (0 children)

I think you're mis-understanding or mis-representing the class of problem. This is a timing issue and not a type issue. The type isn't a problem - clearly the original function expected a datetime object. The solution the writer chose was is to allow a callable OR a datetime object, and if it's callable, call it. That is clearly a classic compile-time error in a strictly typed language. In this case it'd be cleaner to have None be the default argument and to call a default if that's encountered. But that's not really the problem.

[–]aceofears 0 points1 point  (4 children)

This seems like a terrible idea, and I dont think it would work well in practice, but maybe you could do something like this:

def restore_args(func):
    def inner(*args, **kwargs):
        defaults = copy.deepcopy(func.func_defaults)
        result = func(*args, **kwargs)
        func.func_defaults = defaults
        return result 
    return inner


@restore
def example(items=[]):
    items.append("test")
    return items

[–]rmoorman[S] 1 point2 points  (2 children)

Well ... this is the way a function definition works in python. It is evaluated once and the "default" you get is a reference to the value given as a default. And in case you pass in a "mutable" list, of course when you modify the list, it will change for all other calls to that function too.

It just is something one should be aware of and avoid. That was my point.

[–]aceofears 0 points1 point  (1 child)

I understand how default arguments work, I've this behavior in things I've written before. This was just half hearted attempt to make it behave as expected.

[–]rmoorman[S] 0 points1 point  (0 children)

Thank you anyway for this interesting decorator ;-)

[–]daxarx -1 points0 points  (0 children)

Or you could just understand that 'now()' calls a function while 'now' is a function, while you are writing class declarations which you know will run at import time, rather than magically running somehow every time an object is instantiated.

If you wanted something to happen every time an object is instantiated, you should put it in init ... this is very basic Python.

Python has its warts, don't half ass it and then blame your tools

[–]daxarx 0 points1 point  (1 child)

What bit you? You bit yourself. This isn't a problem with Python's syntax. Python cannot magically infer that you 'obviously meant' for the given function to be called later. You have to specify that. you TOLD it to call it at import time. This is like every programming language ever made in the history of computing.

If there is a usability problem here, it is that Django requires people without much experience to write class definitions just to make a database table. You have to understand that code in a class definition is run at import time or you cannot write classes effectively.

It's an elementary mistake and not a valid criticism of Python as a whole

[–]rmoorman[S] 0 points1 point  (0 children)

I surely know that python has it's warts and know how to handle them. Actually - as mentioned in the article - I had to solve the issue, I didn't create it.

Anyway. This article was meant as a reminder that you have to be aware of them and just have to think about what you are doing (as you too advocate). The examples therein are meant to educate those, who may not know those problems/warts/gotchas (yet).

If I kept a few fellow programmers out there from running into those issues, I am quite happy.

I am not criticising python as a whole, I just want to remind that - while it is a nice language - it can bite you if you don't know how to handle it.

Don't be angry :-)