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()
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()
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}")
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)