Close-up of Developer Typing Code on Keyboard

5 Python Automation Scripts for Beginners: A Practical Guide to Automating Repetitive Tasks

Every engineer has a folder full of chaos: screenshots named IMG_20250314_112233.png, invoices scattered across three directories, a dozen PDFs that should be one report. Python is the fastest way out. It ships with a readable syntax and a deep standard library, and a script written on Windows runs on Linux or macOS with minimal changes. That combination is why automation is one of the most common entry points into the language.

This guide walks through five scripts you can build today. Each one solves a real chore, and each introduces a pattern you will reuse: directory traversal, binary file handling, secure credentials, and respectful network requests. The code is deliberately plain. Copy it, run it on a test folder, then adapt it.

Why Python for Automation

Automation is mostly about gluing things together: moving files, calling services, parsing text. Python’s standard library covers all three out of the box with os, shutil, smtplib, and email. Third-party packages fill the gaps for images and PDFs with a single pip install. The syntax reads close to pseudocode, so a beginner can audit a script before trusting it with real data.

Script 1: Bulk File Organizer

The classic first project. Sort files into subfolders by extension, by modification date, or by custom rules. The os module handles listing and paths, shutil handles the moves.

import os
import shutil
from pathlib import Path

RULES = {
    ".jpg": "Images", ".png": "Images", ".gif": "Images",
    ".pdf": "Documents", ".docx": "Documents", ".txt": "Documents",
    ".csv": "Data", ".xlsx": "Data",
    ".zip": "Archives",
}

def organize(folder, dry_run=True):
    folder = Path(folder).expanduser()
    for item in folder.iterdir():
        if not item.is_file():
            continue
        target_name = RULES.get(item.suffix.lower(), "Other")
        target_dir = folder / target_name
        if dry_run:
            print(f"[dry-run] {item.name} -> {target_dir.name}/")
            continue
        try:
            target_dir.mkdir(exist_ok=True)
            shutil.move(str(item), str(target_dir / item.name))
        except (PermissionError, OSError) as exc:
            print(f"Skipped {item.name}: {exc}")

organize(Path.home() / "Downloads", dry_run=True)

Two habits matter here. First, the dry_run flag lets you preview every move before anything touches disk. Second, the try/except block keeps one locked or permission-protected file from aborting the whole run. Run with dry_run=False only after the preview looks right. Note the Path(folder).expanduser() call: Path("~/Downloads") on its own does not expand the tilde, so the script would look for a literal directory named ~. Passing Path.home() / "Downloads" or calling .expanduser() makes the same code work on Windows, Linux, and macOS.

Script 2: Image Resizer and Converter

Pillow (pip install Pillow) turns a folder of oversized photos into web-ready thumbnails. The same loop pattern from Script 1 applies, but now you are working with image objects and metadata.

from pathlib import Path
from PIL import Image

def resize_folder(src, dst, max_width=1200, quality=85):
    src, dst = Path(src), Path(dst)
    dst.mkdir(parents=True, exist_ok=True)
    for path in src.glob("*"):
        if path.suffix.lower() not in {".jpg", ".jpeg", ".png"}:
            continue
        try:
            with Image.open(path) as img:
                ratio = max_width / img.width
                if ratio < 1:
                    new_size = (max_width, int(img.height * ratio))
                    img = img.resize(new_size, Image.LANCZOS)
                img.convert("RGB").save(
                    dst / f"{path.stem}.jpg", "JPEG", quality=quality, optimize=True
                )
        except Exception as exc:
            print(f"Failed on {path.name}: {exc}")

resize_folder("photos/raw", "photos/web")

Saving to JPEG with Pillow does not carry over EXIF metadata unless you pass it explicitly, which is often what you want for public images (it strips GPS coordinates). If you need to preserve metadata, pass exif=img.info.get("exif") to save() (behavior varies by Pillow version; check the Pillow docs for your release). Watermarking follows the same pattern: open a watermark PNG, paste it onto each image with Image.paste(), and save.

Script 3: Email Report Sender

Sending a report on a schedule is one of the highest-value automations you can build. The smtplib and email modules handle it without third-party packages.

import smtplib
import os
from email.message import EmailMessage

def send_report(to_addr, subject, body, attachment_path):
    msg = EmailMessage()
    msg["From"] = os.environ["SMTP_USER"]
    msg["To"] = to_addr
    msg["Subject"] = subject
    msg.set_content(body)

    with open(attachment_path, "rb") as fh:
        msg.add_attachment(
            fh.read(),
            maintype="application",
            subtype="octet-stream",
            filename=os.path.basename(attachment_path),
        )

    with smtplib.SMTP_SSL("smtp.example.com", 465) as server:
        server.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
        server.send_message(msg)

send_report("[email protected]", "Weekly metrics", "See attached.", "report.pdf")

Never hardcode credentials. Read them from environment variables or a secrets manager. If your provider supports app-specific passwords, use one instead of your account password. Many providers use TLS on port 465 or STARTTLS on 587; check your provider’s documentation before assuming a port.

Script 4: Web Scraping for Data Collection

requests and BeautifulSoup (pip install requests beautifulsoup4) cover most static-page scraping. The pattern is fetch, parse, extract.

import time
import requests
from bs4 import BeautifulSoup

HEADERS = {"User-Agent": "MyResearchBot/1.0 ([email protected])"}

def scrape_titles(url):
    resp = requests.get(url, headers=HEADERS, timeout=10)
    resp.raise_for_status()
    soup = BeautifulSoup(resp.text, "html.parser")
    return [h.get_text(strip=True) for h in soup.find_all("h2")]

for page in range(1, 4):
    titles = scrape_titles(f"https://example.com/blog?page={page}")
    print(page, titles)
    time.sleep(2)

Respectful scraping means three things: identify yourself in the User-Agent, honor robots.txt, and rate-limit your requests. The time.sleep(2) above is not decoration; it keeps your traffic from overwhelming a small site. Dynamic pages that render content with JavaScript need a different approach, typically a headless browser or a scraping API. For a sense of how far the batch approach scales, our hands-on test of Scraping 10,000 Pages in Minutes: A Hands-On Test of Olostep’s Batch API measures how long a single Python script took to submit one batch job against MDN’s documentation.

Script 5: PDF Merger and Splitter

PDFs are binary files, so you cannot concatenate them with cat. pypdf (pip install pypdf) reads and writes them properly. PyPDF2 is the legacy predecessor; the maintained package is pypdf.

import sys
from pypdf import PdfWriter, PdfReader

def merge(output, inputs):
    writer = PdfWriter()
    for path in inputs:
        reader = PdfReader(path)
        for page in reader.pages:
            writer.add_page(page)
    with open(output, "wb") as fh:
        writer.write(fh)

def split(input_path, prefix):
    reader = PdfReader(input_path)
    for i, page in enumerate(reader.pages, start=1):
        writer = PdfWriter()
        writer.add_page(page)
        with open(f"{prefix}_{i}.pdf", "wb") as fh:
            writer.write(fh)

if __name__ == "__main__":
    if sys.argv[1] == "merge":
        merge(sys.argv[2], sys.argv[3:])
    elif sys.argv[1] == "split":
        split(sys.argv[2], sys.argv[3])

Note the "wb" mode: binary writes, not text. Passing a string where bytes are expected is a common bug in first PDF scripts. Command-line arguments via sys.argv turn the script into a reusable tool: python pdf_tool.py merge out.pdf a.pdf b.pdf.

Best Practices and Next Steps

The five scripts above share a small set of habits. Carry these forward rather than reinventing them:

  • Preview before you move. The dry_run flag in Script 1 is the pattern to copy everywhere: print what a script would do, read the output, then run for real. Point every new script at a scratch directory first.
  • Wrap per-item work in try/except. Scripts 1 and 2 both skip a single failed file instead of aborting the run. That is what keeps an unattended job alive when it hits a locked file or a corrupt image.
  • Keep credentials out of code. Script 3 reads SMTP_USER and SMTP_PASS from the environment. Do the same for every token, password, and API key.
  • Rate-limit network calls. The time.sleep(2) in Script 4 and the explicit User-Agent are the minimum courtesy for scraping. Add retries and backoff once you move beyond a handful of pages.
  • Schedule with the OS. Cron on Linux and macOS, Task Scheduler on Windows. Both run scripts unattended, and both pair naturally with the logging module so a failed run leaves a timestamped trace.
  • Package for reuse. A pyproject.toml and a clean entry point make scripts shareable. Our guide to Beyond setup.py: Five Best Practices for Building Robust Python AI Libraries covers the failure modes that matter once code leaves your machine: timeouts, malformed input, and network errors.
  • Reach for the right library. schedule gives you in-process job scheduling, and watchdog triggers scripts when files change, which turns the organizer from Script 1 into a live folder monitor. For desktop-level automation on Windows, our breakdown of AutoHotkey v2 Scripts for Data Scientists: Five Windows Shortcuts That Remove the Boring Parts covers a complementary approach.

Start with one script. Run it in dry-run mode, read the output, then let it loose on a real folder. The pattern repeats: observe, automate, schedule. Once the first chore disappears from your week, the second script writes itself.

Similar Posts