DEV Community

Brad
Brad

Posted on

The Python Automation Stack Every Freelancer Should Have (with Code)

The Python Automation Stack Every Freelancer Should Have (with Code)

After 3 years of freelancing, I've automated nearly everything that doesn't require actual thinking. Here's my complete automation stack — free to copy.

The Core Problem

Freelancers waste 15-25% of their time on non-billable admin:

  • Tracking hours
  • Writing invoices
  • Following up on late payments
  • Scheduling calls
  • Onboarding clients
  • Managing files

This is money sitting on the table. Python fixes it.

The Stack

1. Time Tracker (Terminal-Based)

No subscriptions. No apps. Just Python + a CSV file.

import time
import csv
from datetime import datetime
import argparse
import json
from pathlib import Path

class TimeTracker:
    def __init__(self, storage_file="time_log.json"):
        self.storage_file = Path(storage_file)
        self.data = self._load()

    def _load(self):
        if self.storage_file.exists():
            with open(self.storage_file) as f:
                return json.load(f)
        return {"active": None, "entries": []}

    def _save(self):
        with open(self.storage_file, "w") as f:
            json.dump(self.data, f, indent=2, default=str)

    def start(self, project: str, description: str = ""):
        if self.data["active"]:
            self.stop()

        entry = {
            "id": len(self.data["entries"]) + 1,
            "project": project,
            "description": description,
            "start": datetime.now().isoformat(),
            "end": None,
            "duration_minutes": None
        }
        self.data["active"] = entry
        self._save()
        print(f"⏱ Started: {project} - {description}")

    def stop(self):
        if not self.data["active"]:
            print("No active timer")
            return

        entry = self.data["active"]
        entry["end"] = datetime.now().isoformat()

        start = datetime.fromisoformat(entry["start"])
        end = datetime.fromisoformat(entry["end"])
        duration = (end - start).total_seconds() / 60
        entry["duration_minutes"] = round(duration, 1)

        self.data["entries"].append(entry)
        self.data["active"] = None
        self._save()

        print(f"⏹ Stopped: {entry['project']} ({duration:.1f} min)")

    def report(self, project: str = None):
        entries = self.data["entries"]
        if project:
            entries = [e for e in entries if e["project"] == project]

        by_project = {}
        for entry in entries:
            p = entry["project"]
            by_project[p] = by_project.get(p, 0) + entry.get("duration_minutes", 0)

        print("\n=== Time Report ===")
        for proj, minutes in sorted(by_project.items(), key=lambda x: -x[1]):
            hours = minutes / 60
            print(f"  {proj}: {hours:.1f}h ({minutes:.0f} min)")

    def export_csv(self, output_file="timesheet.csv"):
        with open(output_file, "w", newline="") as f:
            fieldnames = ["id", "project", "description", "start", "end", "duration_minutes"]
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerows(self.data["entries"])
        print(f"Exported to {output_file}")

# CLI usage
if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("action", choices=["start", "stop", "report", "export"])
    parser.add_argument("--project", "-p", default="General")
    parser.add_argument("--desc", "-d", default="")
    args = parser.parse_args()

    tracker = TimeTracker()

    if args.action == "start":
        tracker.start(args.project, args.desc)
    elif args.action == "stop":
        tracker.stop()
    elif args.action == "report":
        tracker.report(args.project if args.project != "General" else None)
    elif args.action == "export":
        tracker.export_csv()
Enter fullscreen mode Exit fullscreen mode

Usage: python tracker.py start -p "Client A" -d "Feature work"

2. Invoice Generator

from datetime import datetime, timedelta
from pathlib import Path

def generate_invoice(client, services, invoice_num=None):
    """Generate plain-text invoice"""
    today = datetime.now()
    due_date = today + timedelta(days=30)

    if not invoice_num:
        invoice_num = f"INV-{today.strftime('%Y%m')}-{today.day:02d}"

    total = sum(s["amount"] for s in services)

    invoice = f"""
=====================================
INVOICE #{invoice_num}
=====================================

DATE:     {today.strftime("%B %d, %Y")}
DUE DATE: {due_date.strftime("%B %d, %Y")}

FROM:
{client["freelancer_name"]}
{client["freelancer_email"]}

TO:
{client["name"]}
{client["company"]}
{client["email"]}

-------------------------------------
SERVICES
-------------------------------------
"""

    for service in services:
        invoice += f"""
{service["description"]}
  {service["hours"]}h × ${service["rate"]}/h = ${service["amount"]:.2f}
"""

    invoice += f"""
-------------------------------------
TOTAL: ${total:.2f} USD
-------------------------------------

PAYMENT METHODS:
• Bank Transfer: [Your IBAN]
• PayPal: {client["freelancer_email"]}
• Wise: {client["freelancer_email"]}

NOTES:
Payment due within 30 days.
Late payments subject to 1.5% monthly interest.

Thank you for your business!
"""

    return invoice.strip()
Enter fullscreen mode Exit fullscreen mode

3. Follow-Up Email Scheduler

import smtplib
import json
from email.mime.text import MIMEText
from datetime import datetime, timedelta
from pathlib import Path

def check_and_send_followups(config, invoices_file="invoices.json"):
    """Check overdue invoices and send follow-up emails"""

    with open(invoices_file) as f:
        invoices = json.load(f)

    today = datetime.now().date()
    sent = 0

    for invoice in invoices:
        if invoice.get("paid"):
            continue

        due_date = datetime.strptime(invoice["due_date"], "%Y-%m-%d").date()
        days_overdue = (today - due_date).days

        # Follow-up schedule: 1 day before, day of, 7 days after, 14 days after
        should_follow_up = days_overdue in [-1, 0, 7, 14, 30]
        last_followup = invoice.get("last_followup")

        if should_follow_up and last_followup != str(today):
            send_followup_email(config, invoice, days_overdue)
            invoice["last_followup"] = str(today)
            sent += 1

    # Save updated invoice data
    with open(invoices_file, "w") as f:
        json.dump(invoices, f, indent=2)

    print(f"Sent {sent} follow-up email(s)")
    return sent

def send_followup_email(config, invoice, days_overdue):
    subject_map = {
        -1: f"Reminder: Invoice #{invoice['number']} due tomorrow",
        0: f"Invoice #{invoice['number']} due today",
        7: f"Overdue: Invoice #{invoice['number']} - 7 days past due",
        14: f"Final notice: Invoice #{invoice['number']} - 14 days overdue",
        30: f"URGENT: Invoice #{invoice['number']} - 30 days overdue",
    }

    subject = subject_map.get(days_overdue, f"Invoice #{invoice['number']} overdue {days_overdue} days")

    if days_overdue <= 0:
        tone = "friendly"
        body = f"""Hi {invoice['client_name']},

Just a quick reminder that invoice #{invoice['number']} for ${invoice['amount']:.2f} is due {
    'today' if days_overdue == 0 else 'tomorrow'}.

{config.get('payment_info', 'Payment details in the original invoice.')}

Thanks!
{config['my_name']}"""
    elif days_overdue <= 14:
        body = f"""Hi {invoice['client_name']},

This is a reminder that invoice #{invoice['number']} for ${invoice['amount']:.2f} 
was due {days_overdue} days ago and remains unpaid.

Could you let me know when we can expect payment?

{config.get('payment_info', '')}

Best regards,
{config['my_name']}"""
    else:
        body = f"""Hi {invoice['client_name']},

Invoice #{invoice['number']} for ${invoice['amount']:.2f} is now {days_overdue} days overdue.

Please arrange payment immediately or contact me to discuss.

{config.get('payment_info', '')}

{config['my_name']}"""

    msg = MIMEText(body, "plain")
    msg["From"] = config["email"]
    msg["To"] = invoice["client_email"]
    msg["Subject"] = subject

    with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
        smtp.login(config["email"], config["password"])
        smtp.send_message(msg)

    print(f"  Sent: {subject}")
Enter fullscreen mode Exit fullscreen mode

The Productivity Math

Task Manual time Automated time Saved
Time tracking 15 min/day 0 min 7.5h/month
Invoice creation 30 min each 2 min 2h/month
Follow-ups 10 min each 0 min 1h/month
File organization 20 min/client 1 min 1h/month
Total 11.5h/month ~30min ~11h

At $100/hour: $1,100/month recovered.

Getting Started

Pick ONE script. The time tracker is easiest — you can start using it today with zero setup.

Run it for a week. See where your hours actually go. Then automate the next biggest time sink.


Want all of these scripts, plus 10 more, pre-configured and ready to use?

Get the Python Business Automation Toolkit →

One download, immediate access. Includes setup instructions and config templates.

Which of these scripts would save you the most time? Let me know in the comments.

Top comments (0)