129

I have a long running python script that I want to do someting at 01:00 every morning.

I have been looking at the sched module and at the Timer object but I can't see how to use these to achieve this.

Anderson Green
  • 30,230
  • 67
  • 195
  • 328
Paul McKenzie
  • 19,646
  • 25
  • 76
  • 120

4 Answers4

327

I spent quite a bit of time also looking to launch a simple Python program at 01:00. For some reason, I couldn't get cron to launch it and APScheduler seemed rather complex for something that should be simple. Schedule (https://pypi.python.org/pypi/schedule) seemed about right.

You will have to install their Python library:

pip install schedule

This is modified from their sample program:

import schedule
import time

def job(t):
    print "I'm working...", t
    return

schedule.every().day.at("01:00").do(job,'It is 01:00')

while True:
    schedule.run_pending()
    time.sleep(60) # wait one minute

You will need to put your own function in place of job and run it with nohup, e.g.:

nohup python2.7 MyScheduledProgram.py &

Don't forget to start it again if you reboot.

user2099484
  • 4,417
  • 2
  • 21
  • 9
  • 100
    Anyone seeing this thread in future, hope they understand that this is the right solution for a production code rather than the accepted one. – yeaske Dec 20 '17 at 20:20
  • 1
    Will this work on mac, linux and windows ? Or this script needs some modification? @yeaske – pramodpxi Mar 05 '18 at 03:49
  • I use it on linux and it should work on mac. Windows, I am not sure. It is no great investment, however, to set it up and test carefully. I would advise that in any event since schedulers are a bit tricky. – user2099484 Mar 06 '18 at 08:27
  • I can also attest to Mac and unix. Haven't tried on windows. – yeaske Mar 06 '18 at 21:17
  • 3
    This library works great on Windows too! – Ben.T Mar 22 '18 at 15:32
  • this package includes some unexpected behavior. See: https://github.com/dbader/schedule/issues/268 – anon01 Jan 08 '19 at 18:07
  • 1
    It works but `While block` prevents other code lines to run, any way to fix this? – Salem Masoud Mar 10 '19 at 19:03
  • If you are running this on a multi-user operating system like Linux, it does not block other programs running, especially while it is sleeping. I use a version of this on a busy Linux system. – user2099484 Mar 11 '19 at 20:09
  • 3
    WARNING: if you are running multiple applications at the same time e.g. Flask app, this scheduler will block. If you want to run proceses on parallel or in the background as a Daemon you still need to use something like APScheduler. So I don't think this is always the right solution. – carkod Feb 14 '21 at 01:30
93

You can do that like this:

from datetime import datetime
from threading import Timer

x=datetime.today()
y=x.replace(day=x.day+1, hour=1, minute=0, second=0, microsecond=0)
delta_t=y-x

secs=delta_t.seconds+1

def hello_world():
    print "hello world"
    #...

t = Timer(secs, hello_world)
t.start()

This will execute a function (eg. hello_world) in the next day at 1a.m.

EDIT:

As suggested by @PaulMag, more generally, in order to detect if the day of the month must be reset due to the reaching of the end of the month, the definition of y in this context shall be the following:

y = x.replace(day=x.day, hour=1, minute=0, second=0, microsecond=0) + timedelta(days=1)

With this fix, it is also needed to add timedelta to the imports. The other code lines maintain the same. The full solution, using also the total_seconds() function, is therefore:

from datetime import datetime, timedelta
from threading import Timer

x=datetime.today()
y = x.replace(day=x.day, hour=1, minute=0, second=0, microsecond=0) + timedelta(days=1)
delta_t=y-x

secs=delta_t.total_seconds()

def hello_world():
    print "hello world"
    #...

t = Timer(secs, hello_world)
t.start()
sissi_luaty
  • 2,839
  • 21
  • 28
  • 9
    I need to do this *every* morning ... – Paul McKenzie Feb 26 '13 at 14:02
  • 2
    The callback can start another `Timer` when it's first called. – Brigand Feb 26 '13 at 14:31
  • 7
    fix the date logic: x = datetime.today() y = (x + timedelta(days=1)).replace(hour=2, minute=0, second=0) delta_t = y - x – Paul McKenzie Mar 01 '13 at 13:45
  • so if I want to call the function at 1:55 pm every dat, I would change it to this, right?: y=x.replace(day=x.day+1, hour=13, minute=55, second=0, microsecond=0) – Haiz Jul 05 '15 at 06:02
  • great man really good : How can we get that specific day on every month – naveenkumar.s Jun 08 '16 at 12:04
  • 1
    @naveenkumar.s use, for instance: today = datetime.today() my_birthday = datetime(today.year, 11, 02) if my_birthday – sissi_luaty Jun 08 '16 at 16:14
  • 2
    Nice solution. Just a question, why do you add 1 second on the row `secs = delta_t.seconds+1`? – Sahand Oct 28 '16 at 11:32
  • 3
    @Sandi good question! It's because with x=datetime.today(), x can have 0-999999microsecs (apart from the other values); then, using the seconds of x-y would give a result that would begin 0-999999microsecs before the supposed date; with +1 the function will start at 0-999999microsecs after the supposed date. It's similar to a ceil() rounding of the seconds. However, that "1" can be changed, if more precision is required, to "0.000001*delta_t.microseconds". – sissi_luaty Nov 15 '16 at 18:22
  • 1
    You should use `delta_t.total_seconds()` instead of `delta_t.seconds`. Otherwise if `delta_t` is more than one day, e.g. 1 day + some milliseconds, the task will be repeated multiple times per second. – pcworld Nov 01 '18 at 00:30
  • Does anyone know if we require the computer to be turned on for this? – Patriots299 Dec 31 '18 at 21:30
  • 1
    @Patriots299 what do you mean? – redbeam_ Apr 09 '19 at 08:21
  • @Patriots299 Do you mean if it is possible to start the timer, restart computer, and have the timer continue? I highly doubt that. This just starts a process, and when you turn off your computer every process is turned off. There is nothing that starts it automatically when you torn your PC on again. But if DO want it to be turned on automatically when starting your PC you should look up how to start scripts at startup. If Windows, you can start f.ex. here: https://superuser.com/questions/954950/run-a-script-on-start-up-on-windows-10 – PaulMag Apr 25 '19 at 10:34
  • 2
    This breaks when you reach the end of the month and give `ValueError: day is out of range for month`, because it blindly increments the day without considering the month, so you end up with day=32. Instead do `x.replace(day=x.day, hour=1, minute=0, second=0, microsecond=0) + datetime.timedelta(days=1)`. Then datetime understands to add a logical day and will increment the month and year as necessary such that it can go on until the year is over 9000. Credit: https://stackoverflow.com/a/3240486/3956460 – PaulMag Apr 25 '19 at 11:20
  • 1
    @PaulMag - I have updated the answer, thank you! – sissi_luaty Apr 30 '19 at 21:33
  • `threading.Timer` starts a new thread. This adds unnecessary complexity unless you need to do something else while waiting for the task to start. – cambunctious Jan 13 '21 at 18:00
19

APScheduler might be what you are after.

from datetime import date
from apscheduler.scheduler import Scheduler

# Start the scheduler
sched = Scheduler()
sched.start()

# Define the function that is to be executed
def my_job(text):
    print text

# The job will be executed on November 6th, 2009
exec_date = date(2009, 11, 6)

# Store the job in a variable in case we want to cancel it
job = sched.add_date_job(my_job, exec_date, ['text'])

# The job will be executed on November 6th, 2009 at 16:30:05
job = sched.add_date_job(my_job, datetime(2009, 11, 6, 16, 30, 5), ['text'])

https://apscheduler.readthedocs.io/en/latest/

You can just get it to schedule another run by building that into the function you are scheduling.

Paul Collingwood
  • 9,053
  • 3
  • 23
  • 36
11

I needed something similar for a task. This is the code I wrote: It calculates the next day and changes the time to whatever is required and finds seconds between currentTime and next scheduled time.

import datetime as dt

def my_job():
    print "hello world"
nextDay = dt.datetime.now() + dt.timedelta(days=1)
dateString = nextDay.strftime('%d-%m-%Y') + " 01-00-00"
newDate = nextDay.strptime(dateString,'%d-%m-%Y %H-%M-%S')
delay = (newDate - dt.datetime.now()).total_seconds()
Timer(delay,my_job,()).start()
Ankit
  • 131
  • 1
  • 3
  • Hi, I understand that you find the seconds between tomorrow versus today, but can you explain the part there you put it in a 'Timer()' What does this line mean with the parameters, (). I looked for the Timer and found this: https://pypi.org/project/timer3/ Do you need to install timer3 for this? Thanks! – user3553260 May 21 '19 at 00:42
  • Hi, you don't need timer3 library. I used Timer from threading. [here is the reference](https://docs.python.org/2/library/threading.html#timer-objects). The third argument is to pass arguments to your function i.e. arguments you want Timer to send to your function on call. – Ankit May 22 '19 at 08:33
  • [Here](https://stackoverflow.com/questions/16578652/threading-timer#16578710) is another reference for Timer library usage – Ankit May 22 '19 at 08:33