Skip to content
Educora
Advanced20 min28 / 42

Automation scripts

Hand routine work over to Python: sort files with `pathlib` and `shutil`, find old files with `datetime`, build a command-line interface with `argparse`, keep a log with `logging`, and run the script on schedule with cron or Task Scheduler.

Check yourself
In this lesson you will learn
  • Safely create, move and copy files and folders with pathlib, os and shutil
  • Format and compare dates and compute file ages with datetime and timedelta
  • Write a professional command-line script with argparse and logging and run it on a schedule

Leyla's Downloads folder holds hundreds of files in a mess, and every evening she copies her documents to a backup drive by hand. That is a few minutes every day and a backup that is sometimes forgotten. A 25-line Python script sorts the files into folders in seconds, and the system scheduler runs it every day by itself. In this lesson we will build every part of such a script.

Files and folders: pathlib, os, shutil

TaskCode
folder contents / including subfoldersPath.iterdir(), Path.rglob('*.pdf'), os.walk()
create a folderPath.mkdir(parents=True, exist_ok=True)
copy a file / a foldershutil.copy2(src, dst), shutil.copytree(src, dst)
move or renameshutil.move(src, dst), Path.rename(new)
delete a file / a whole folderPath.unlink(), shutil.rmtree(path)

The script below sorts files into folders by their extension. To avoid touching your real files, we create a temporary folder with tempfile.mkdtemp() and delete it at the end. Note that .JPG and .jpg are treated the same, because we lowercase the extension with lower().

Python
import shutil
import tempfile
from pathlib import Path

root = Path(tempfile.mkdtemp())
for name in ['report.pdf', 'photo1.jpg', 'photo2.JPG', 'notes.txt', 'data.csv', 'song.mp3']:
    (root / name).write_text('demo')

folders = {'.pdf': 'documents', '.txt': 'documents', '.jpg': 'images', '.csv': 'data'}
for file in list(root.iterdir()):
    if file.is_file():
        target = root / folders.get(file.suffix.lower(), 'other')
        target.mkdir(exist_ok=True)
        shutil.move(file, target / file.name)

for path in sorted(root.rglob('*')):
    print(path.relative_to(root).as_posix())
shutil.rmtree(root)
print(root.exists())
▸ Expected output
data
data/data.csv
documents
documents/notes.txt
documents/report.pdf
images
images/photo1.jpg
images/photo2.JPG
other
other/song.mp3
False
list(root.iterdir()) takes the list in advance so that changing the folder during the loop is safe. Unknown extensions go to the other folder.

Dates and times

The datetime module has date, datetime and timedelta for durations. strftime turns a date into formatted text (%Y year, %m month, %d day, %H:%M time), and strptime does the reverse, from text to a date. To keep the results reproducible, the examples use a fixed date instead of datetime.now().

Python
from datetime import date, datetime, timedelta

now = datetime(2026, 9, 27, 14, 30)
print(now.strftime('%Y-%m-%d_%H-%M'))
print(now.strftime('%d.%m.%Y'))
deadline = now + timedelta(days=10, hours=3)
print(deadline.isoformat(sep=' '))
print((date(2026, 12, 31) - now.date()).days, 'days left in the year')
parsed = datetime.strptime('05.10.2026 09:15', '%d.%m.%Y %H:%M')
print(parsed, parsed.strftime('%A'))
▸ Expected output
2026-09-27_14-30
27.09.2026
2026-10-07 17:30:00
95 days left in the year
2026-10-05 09:15:00 Monday

A file's last modification time is given by stat().st_mtime (seconds since 1970). We turn it into a date with datetime.fromtimestamp and compare it with a limit. In the example we set the files' “age” artificially with os.utime:

Python
import os
import tempfile
from datetime import datetime, timedelta
from pathlib import Path

root = Path(tempfile.mkdtemp())
now = datetime(2026, 9, 27, 12, 0)
for name, days in {'backup_old.zip': 45, 'log_week.txt': 7, 'log_today.txt': 0}.items():
    f = root / name
    f.write_text('x')
    t = (now - timedelta(days=days)).timestamp()
    os.utime(f, (t, t))

limit = now - timedelta(days=30)
for f in sorted(root.iterdir()):
    modified = datetime.fromtimestamp(f.stat().st_mtime)
    status = 'OLD' if modified < limit else 'ok'
    print(f'{f.name:15} {modified:%Y-%m-%d} {status}')
▸ Expected output
backup_old.zip  2026-08-13 OLD
log_today.txt   2026-09-27 ok
log_week.txt    2026-09-20 ok

argparse and logging

A good script does not hard-code paths and numbers — it takes them from the command line: python cleanup.py downloads --days 14 --dry-run. argparse parses positional arguments (folder), options (--days) and flags (--dry-run), converts types and builds the --help page by itself. In a real script parse_args() is called without arguments; here we imitate the command line with a list.

Python
import argparse

parser = argparse.ArgumentParser(prog='cleanup', description='List or delete old files.')
parser.add_argument('folder', help='folder to scan')
parser.add_argument('--days', type=int, default=30, help='age limit in days')
parser.add_argument('--dry-run', action='store_true', help='only show what would happen')

args = parser.parse_args(['downloads', '--days', '14', '--dry-run'])
print(args)
print(args.folder, args.days, args.dry_run)
print(parser.parse_args(['photos']))
▸ Expected output
Namespace(folder='downloads', days=14, dry_run=True)
downloads 14 True
Namespace(folder='photos', days=30, dry_run=False)
--dry-run becomes the attribute dry_run (the hyphen turns into an underscore). In the second call the options that were not given take their default values.

Nobody watches the screen of a scheduled script, so logging is used instead of print: every message has a level (DEBUG, INFO, WARNING, ERROR, CRITICAL) and a time, messages can go to a file, and unimportant ones can be filtered out. The placeholders %s and %d format the message only when it is actually printed.

Python
import logging
import sys

logging.basicConfig(stream=sys.stdout, level=logging.INFO,
                    format='%(levelname)-8s %(name)s: %(message)s', force=True)
log = logging.getLogger('backup')
log.debug('not shown: the level is INFO')
log.info('Backup started')
log.warning('Disk is 85% full')
try:
    ratio = 10 / 0
except ZeroDivisionError:
    log.error('Could not compute the ratio')
log.info('Copied %d files in %.1f s', 128, 3.42)
▸ Expected output
INFO     backup: Backup started
WARNING  backup: Disk is 85% full
ERROR    backup: Could not compute the ratio
INFO     backup: Copied 128 files in 3.4 s
Here the log goes to the screen (sys.stdout); in a real script you pass filename='backup.log' and add %(asctime)s (the time) to the format. The DEBUG message is below the INFO level, so it is not shown.

A complete script and scheduling it

Python
import argparse
import logging
import shutil
from datetime import datetime
from pathlib import Path

log = logging.getLogger('backup')

def backup(source, dest_root, dry_run=False):
    stamp = datetime.now().strftime('%Y-%m-%d_%H-%M')
    target = Path(dest_root) / f'{Path(source).name}_{stamp}'
    log.info('Copying %s -> %s (dry run: %s)', source, target, dry_run)
    if not dry_run:
        shutil.copytree(source, target)
    return target

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Back up a folder with a timestamp.')
    parser.add_argument('source', help='folder to copy')
    parser.add_argument('dest', help='where to put the copy')
    parser.add_argument('--dry-run', action='store_true', help='only log, do not copy')
    args = parser.parse_args()
    logging.basicConfig(filename='backup.log', level=logging.INFO,
                        format='%(asctime)s %(levelname)s %(message)s')
    backup(args.source, args.dest, args.dry_run)
backup.py. The if __name__ == '__main__': block runs only when the file is started directly — the backup function can be imported into other scripts and tests. On your computer: python backup.py Documents D:/Backup --dry-run, then look at backup.log.

To run a script regularly, use the operating system's scheduler: cron on Linux and macOS and Task Scheduler on Windows (schtasks on the command line). A cron line has five fields: minute, hour, day of month, month and day of week; * means “any”.

cron expressionWhen
0 21 * * *every day at 21:00
*/15 * * * *every 15 minutes
0 9 * * 1-5on weekdays at 09:00
0 3 1 * *on the 1st of every month at 03:00
Terminal
# Linux / macOS: open the table with `crontab -e` and add a line
0 21 * * * /home/leyla/.venv/bin/python /home/leyla/scripts/backup.py /home/leyla/Documents /mnt/backup

# Windows (Command Prompt): the same task every day at 21:00
schtasks /Create /SC DAILY /ST 21:00 /TN "Daily backup" /TR "C:\scripts\.venv\Scripts\python.exe C:\scripts\backup.py C:\Users\Leyla\Documents D:\Backup"
There is also a Python library called schedule (pip install schedule), but with it the script has to stay running all the time; the system scheduler keeps working even after the computer restarts.
Exercise

Group the file names by extension: take the extension in lowercase and use '(none)' for files without one. Print the extensions in sorted order, each with its sorted list of files, on separate lines.

Exercise · Python
from pathlib import PurePath

files = ['report.PDF', 'photo.jpg', 'notes.txt', 'scan.pdf', 'song.mp3', 'README']
# group by lowercase extension, then print: extension [sorted names]
▸ Expected output
(none) ['README']
.jpg ['photo.jpg']
.mp3 ['song.mp3']
.pdf ['report.PDF', 'scan.pdf']
.txt ['notes.txt']
Exercise

Today is 2026-09-27. For every file older than 30 days print its name and how many days ago it was modified (in the dictionary's order). Finally print the newest file as newest: <name>.

Exercise · Python
from datetime import date

today = date(2026, 9, 27)
files = {'backup.zip': '2026-07-30', 'notes.txt': '2026-09-20',
         'photo.jpg': '2026-08-15', 'log.txt': '2026-09-27'}
# print old files with their age in days, then the newest file
▸ Expected output
backup.zip 59
photo.jpg 43
newest: log.txt

Key points

  • pathlib handles paths and files, and shutil copies, moves and deletes folders; deleted files do not come back.
  • datetime, timedelta, strftime and strptime; write dates in file names as YYYY-MM-DD.
  • argparse gives you positional arguments, options, flags and an automatic --help.
  • In scheduled scripts use logging, with levels and timestamps, instead of print.
  • cron or Task Scheduler runs the script regularly; use absolute paths, the full python path and the --dry-run habit.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
Which function permanently deletes a whole folder with everything inside?