Skip to content

Conversation

@dreackm
Copy link

@dreackm dreackm commented Nov 5, 2025

Closes #1472

This PR adds native timezone support to Procrastinate’s periodic task scheduler.

The Blueprint.periodic decorator now accepts a tzinfo argument, which can be either a string or a zoneinfo.ZoneInfo object. When provided, croniter will evaluate the cron expression in the specified timezone rather than defaulting to UTC.

Previously, users had to manually convert local times to UTC to ensure tasks ran at the intended hours. This often led to confusion and misaligned schedules. With this enhancement, users can define periodic tasks directly in their local timezone, improving predictability and reducing the risk of scheduling errors.

Key Changes

  • The tzinfo attribute is now set on the croniter instance within PeriodicTask.

Example Usage

from zoneinfo import ZoneInfo

tzinfo = "Africa/Blantyre" or ZoneInfo("Africa/Blantyre")  # UTC+2

@app.periodic(cron="* 8-17 * * *", tzinfo=tzinfo, ...)
@app.task(...)
def between_5_and_7(timestamp): ...

Without tzinfo: runs from 10 AM to 7 PM UTC
With tzinfo: runs from 8 AM to 5 PM local time

Successful PR Checklist:

  • Tests
    • (not applicable?)
  • Documentation
    • (not applicable?)

PR label(s):

@dreackm dreackm requested a review from a team as a code owner November 5, 2025 23:36
@github-actions github-actions bot added the PR type: feature ⭐️ Contains new features label Nov 5, 2025
@dreackm dreackm marked this pull request as draft November 5, 2025 23:39
@dreackm dreackm marked this pull request as ready for review November 5, 2025 23:42
@github-actions
Copy link

github-actions bot commented Nov 6, 2025

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  procrastinate
  blueprints.py
  periodic.py 51-55, 57
Project Total  

This report was generated by python-coverage-comment-action

Copy link
Member

@ewjoachim ewjoachim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work so far, we're missing tests. Also, I'd prefer we reach a consensus on the issue before merging the PR, as we may decide on a slightly different API to mitigate the risks.

Comment on lines +54 to +55
logger.error(f"{tzinfo} is not a valid timezone.")
return None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A failure could be impactful, I think we need to raise.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only issue I can see is that it might confuse users when their chosen timezone isn’t applied. Besides that, does it have any other impact?

And if we’re okay with it crashing, wouldn’t it be simpler not to catch the error in the first place?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only issue I can see is that it might confuse users when their chosen timezone isn’t applied. Besides that, does it have any other impact?

If I have scheduled batch processing at 12AM while there's no activity, and it happens at 2PM during peak hours, it could easily cause disruption.

cron: str,
periodic_id: str,
configure_kwargs: tasks.ConfigureTaskOptions,
tzinfo: str | None | ZoneInfo = None,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Please put None last in the types)

cron: str
periodic_id: str
configure_kwargs: tasks.ConfigureTaskOptions
tzinfo: str | None | ZoneInfo = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather we convert str to ZoneInfo at constructor time, and we only ever store ZoneInfo (or None) on the object

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meaning we shouldn't accept a ZoneInfo object?

Other than being simpler for the user, do you have any other specific reason for restricting it to a string value?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah no, I meant: whether we recieve a str or a ZoneInfo, what we store is a ZoneInfo.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was confused. Okay.

return croniter.croniter(self.cron)
croniter_instance = croniter.croniter(self.cron)
# croniter sets the timezone info object in
croniter_instance.tzinfo = self.get_tzinfo()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that the public API of croniter ? I couldn't find the official doc, so it's hard to say.

Also, I believe, support of ZoneInfo is recent, which means we need to severly restrict the accepted versions in pyproject.toml (which might create some conflict, but the lib didn't move a lot)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realise the comment was incomplete. It's not the public API, but at least it's not a private attribute. They set tzinfo attribute in the initialiser of the croniter instance.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you mean by the ZoneInfo support being recent? As I believe it's supported in v6.0.0 (from December, 2024)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will see if I run into any issues once I write tests.

Timezone in which the cron expression should be interpreted. Accepts a
timezone name string (e.g., "Africa/Blantyre"), a `zoneinfo.ZoneInfo` instance,
or `None`. When `None` (the default), the underlying `croniter` library
will interpret the schedule in UTC (the current behaviour).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mention (the current behaviour) makes sense as a PR comment because we're talking code evolution, but doesn't make sense in the docstring. In 2 years time, what will "current" refer to ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense. It's indeed confusing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR type: feature ⭐️ Contains new features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Setting timezone for periodic task

2 participants