78

I'm having some weird issues with pytz's .localize() function. Sometimes it wouldn't make adjustments to the localized datetime:

.localize behaviour:

>>> tz
<DstTzInfo 'Africa/Abidjan' LMT-1 day, 23:44:00 STD> 
>>> d
datetime.datetime(2009, 9, 2, 14, 45, 42, 91421)

>>> tz.localize(d)
datetime.datetime(2009, 9, 2, 14, 45, 42, 91421, 
                  tzinfo=<DstTzInfo 'Africa/Abidjan' GMT0:00:00 STD>)
>>> tz.normalize(tz.localize(d))
datetime.datetime(2009, 9, 2, 14, 45, 42, 91421,
                  tzinfo=<DstTzInfo 'Africa/Abidjan' GMT0:00:00 STD>)

As you can see, time has not been changed as a result of localize/normalize operations. However, if .replace is used:

>>> d.replace(tzinfo=tz)
datetime.datetime(2009, 9, 2, 14, 45, 42, 91421, 
                  tzinfo=<DstTzInfo 'Africa/Abidjan' LMT-1 day, 23:44:00 STD>)
>>> tz.normalize(d.replace(tzinfo=tz))
datetime.datetime(2009, 9, 2, 15, 1, 42, 91421,
                  tzinfo=<DstTzInfo 'Africa/Abidjan' GMT0:00:00 STD>)

Which seems to make adjustments into datetime.

Question is - which is correct and why other's wrong?

1

4 Answers 4

62

localize just assumes that the naive datetime you pass it is "right" (except for not knowing about the timezone!) and so just sets the timezone, no other adjustments.

You can (and it's advisable...) internally work in UTC (rather than with naive datetimes) and use replace when you need to perform I/O of datetimes in a localized way (normalize will handle DST and the like).

Sign up to request clarification or add additional context in comments.

5 Comments

quote Alex for the suggestion of using UTC and localize/delocalize during I/O operations. May I suggest that is not advisable but strongly recommended (read obliged)!
The OP asked about the difference between the localize and replace. Doesn't replace also just set the timezone, making no other adjustments? If so, why are there differences between the two results?
@MichaelWaterfall: pytz.timezone() may correspond to several tzinfo objects (same place, different UTC offsets, timezone abbreviations). tz.localize(d) tries to find the correct tzinfo for the given d local time (some local time is ambiguous or doesn't exist). replace() just sets whatever (random) info pytz timezone provides by default without regard for the given date (LMT in recent versions). tz.normalize() may adjust the time if d is a non-existent local time e.g., the time during DST transition in Spring (northern hemisphere) otherwise it does nothing in this case.
replace(tzinfo = ...) therefore seems useless. Its probably best to avoid it.
@paolov replace is not completely useless, it just makes certain assumptions about the way that tzinfo objects work that aren't followed by pytz. I think it works for the new ZoneInfo objects that were introduced in Python 3.9 for example.
38

localize is the correct function to use for creating datetime aware objects with an initial fixed datetime value. The resulting datetime aware object will have the original datetime value. A very common usage pattern in my view, and one that perhaps pytz can better document.

replace(tzinfo = ...) is unfortunately named. It is a function that is random in its behaviour. I would advise avoiding the use of this function to set timezones unless you enjoy self-inflicted pain. I have already suffered enough from using this function.

6 Comments

Can't really argue with that.
Complete agreement. Dealing with an issue now where replace() is not functioning at all, but raises no error. Just does nothing. Need a better way to force a naive datetime object to UTC.
I have been through replace(tzinfo=...). I feel your pain.
For those reading along in a 'post-pytz' world (where localize() no longer exists): I've had similar bad experiences with datetime.replace(tz=...) in the past. However it seems in the case of using a PEP 495-compatible timezone (e.g. ZoneInfo), datetime.replace(tz=...) behaves well enough for the community to have accepted using it again. For example, Django's make_aware() simply wraps datetime.replace() (but still uses localize() if it sees that you're still using pytz). It's also recommended here: pytz-deprecation-shim.readthedocs.io/en/latest/migration.html
pytz.localize is literally a thin wrapper around datetime.replace() github.com/stub42/pytz/blob/master/src/pytz/__init__.py#L423
|
13

This DstTzInfo class is used for timezones where the offset from UTC changes at certain points in time. For example (as you are probably aware), many locations transition to "daylight savings time" at the beginning of Summer, and then back to "standard time" at the end of Summer. Each DstTzInfo instance only represents one of these timezones, but the "localize" and "normalize" methods help you get the right instance.

For Abidjan, there has only ever been one transition (according to pytz), and that was in 1912:

>>> tz = pytz.timezone('Africa/Abidjan')
>>> tz._utc_transition_times
[datetime.datetime(1, 1, 1, 0, 0), datetime.datetime(1912, 1, 1, 0, 16, 8)]

The tz object we get out of pytz represents the pre-1912 timezone:

>>> tz
<DstTzInfo 'Africa/Abidjan' LMT-1 day, 23:44:00 STD>

Now looking up at your two examples, see that when you call tz.localize(d) you do NOT get this pre-1912 timezone added to your naive datetime object. It assumes that the datetime object you give it represents local time in the correct timezone for that local time, which is the post-1912 timezone.

However in your second example using d.replace(tzinfo=tz), it takes your datetime object to represent the time in the pre-1912 timezone. This is probably not what you meant. Then when you call dt.normalize it converts this to the timezone that is correct for that datetime value, ie the post-1912 timezone.

Comments

8

I realize I'm a little late on this... but here is what I found to work well. Work in UTC as Alex stated:

tz = pytz.timezone('Africa/Abidjan')
now = datetime.datetime.utcnow()

Then to localize:

tzoffset = tz.utcoffset(now)
mynow = now+tzoffset

And this method does handle DST perfectly

3 Comments

it is incorrect if .utcoffset() method expects datetime in local timezone that is different from utc. To get the current time in local timezone: now = datetime.now(tz). To convert utc time to the local timezone: dt = utc_dt.replace(tzinfo=pytz.utc).astimezone(tz) (you don't need .localize(), .normalize() here).
It can also be incorrect if the offset crosses a DST boundary
This is misleading. Use aware datetime to represent datetime in a specific time zone. To get now in a certain time zone, supply a tz to datetime.now. To get now UTC, supply tz=UTC. Naive datetime on the other hand will always be interpreted as local time (tz setting of OS). And with Python 3.9, you have zoneinfo to handle time zones - no need to worry about localize vs. replace.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.