Toolmingo
Guides20 min read

25 Python Projects for Beginners (with Code) — 2025 Guide

25 hands-on Python projects for beginners to intermediate developers — with starter code, what you learn, difficulty ratings, and portfolio tips for 2025.

Building projects is the fastest way to learn Python. Reading tutorials gives you syntax — projects give you intuition, debugging experience, and something real to show employers. This guide walks through 25 Python projects organized by difficulty, with starter code, what you learn, and time estimates.

At a glance

Level Projects Time each Key skills
Beginner #1–8 2–6 hours Variables, loops, functions, I/O
Intermediate #9–16 1–3 days OOP, APIs, file handling, data
Advanced #17–25 1–2 weeks Web, automation, ML, async

Tip: Don't skip beginners if you're new — even the "simple" projects teach critical habits like structuring code, handling edge cases, and writing readable functions.


Beginner projects (#1–8)

#1 — Number guessing game

What you build: The computer picks a random number; the user guesses until they find it. The program gives "higher / lower" hints and counts attempts.

What you learn: random, while loops, input(), conditionals, basic user interaction.

Time: 1–2 hours | Difficulty:

import random

def guessing_game():
    secret = random.randint(1, 100)
    attempts = 0

    print("Guess the number between 1 and 100!")

    while True:
        guess = int(input("Your guess: "))
        attempts += 1

        if guess < secret:
            print("Too low!")
        elif guess > secret:
            print("Too high!")
        else:
            print(f"Correct! You got it in {attempts} attempt(s).")
            break

guessing_game()

Extensions: Add a difficulty setting (range 1–10, 1–1000), limit attempts, save high scores to a file.


#2 — To-do list (CLI)

What you build: A command-line app where users can add, view, complete, and delete tasks. Tasks persist to a JSON file between sessions.

What you learn: Lists, dicts, file I/O with json, CRUD patterns, while menus.

Time: 2–3 hours | Difficulty:

import json
import os

TASKS_FILE = "tasks.json"

def load_tasks():
    if os.path.exists(TASKS_FILE):
        with open(TASKS_FILE) as f:
            return json.load(f)
    return []

def save_tasks(tasks):
    with open(TASKS_FILE, "w") as f:
        json.dump(tasks, f, indent=2)

def show_tasks(tasks):
    if not tasks:
        print("No tasks yet.")
        return
    for i, task in enumerate(tasks, 1):
        status = "✓" if task["done"] else "○"
        print(f"{i}. [{status}] {task['name']}")

def main():
    tasks = load_tasks()
    while True:
        print("\n1) Add  2) View  3) Complete  4) Delete  5) Quit")
        choice = input("> ").strip()
        if choice == "1":
            name = input("Task name: ").strip()
            tasks.append({"name": name, "done": False})
            save_tasks(tasks)
        elif choice == "2":
            show_tasks(tasks)
        elif choice == "3":
            show_tasks(tasks)
            idx = int(input("Complete task #: ")) - 1
            tasks[idx]["done"] = True
            save_tasks(tasks)
        elif choice == "4":
            show_tasks(tasks)
            idx = int(input("Delete task #: ")) - 1
            tasks.pop(idx)
            save_tasks(tasks)
        elif choice == "5":
            break

main()

#3 — Calculator

What you build: A CLI calculator supporting +, −, ×, ÷, power, and square root. Handles division by zero and invalid input.

What you learn: Functions, error handling with try/except, match statement (Python 3.10+).

Time: 1–2 hours | Difficulty:

import math

def calculate(a, op, b=None):
    match op:
        case "+": return a + b
        case "-": return a - b
        case "*": return a * b
        case "/":
            if b == 0:
                raise ValueError("Cannot divide by zero")
            return a / b
        case "**": return a ** b
        case "sqrt": return math.sqrt(a)
        case _: raise ValueError(f"Unknown operator: {op}")

# Example usage
print(calculate(9, "sqrt"))    # 3.0
print(calculate(10, "/", 2))  # 5.0

#4 — Password generator

What you build: Generate strong random passwords with configurable length and character sets (uppercase, lowercase, digits, symbols).

What you learn: random, string module, function arguments with defaults, list comprehensions.

Time: 1–2 hours | Difficulty:

import random
import string

def generate_password(length=16, use_upper=True, use_digits=True, use_symbols=True):
    chars = string.ascii_lowercase
    if use_upper:
        chars += string.ascii_uppercase
    if use_digits:
        chars += string.digits
    if use_symbols:
        chars += string.punctuation

    return "".join(random.choice(chars) for _ in range(length))

print(generate_password(20))
print(generate_password(8, use_symbols=False))

Extensions: Copy to clipboard with pyperclip, check password strength, build a simple Tkinter GUI.


#5 — Rock-paper-scissors

What you build: Play rock-paper-scissors against the computer with score tracking across multiple rounds.

What you learn: Dictionaries for game logic, random.choice, functions, f-strings.

Time: 1–2 hours | Difficulty:

import random

CHOICES = ["rock", "paper", "scissors"]
WINS = {"rock": "scissors", "paper": "rock", "scissors": "paper"}

def play_round(player):
    computer = random.choice(CHOICES)
    print(f"Computer chose: {computer}")
    if player == computer:
        return "tie"
    return "win" if WINS[player] == computer else "lose"

scores = {"win": 0, "lose": 0, "tie": 0}
while True:
    choice = input("Enter rock/paper/scissors (or q to quit): ").lower()
    if choice == "q":
        break
    if choice not in CHOICES:
        print("Invalid choice")
        continue
    result = play_round(choice)
    scores[result] += 1
    print(f"You {result}! Score — W:{scores['win']} L:{scores['lose']} T:{scores['tie']}")

#6 — Mad libs generator

What you build: Read a template story with placeholders, prompt the user for words (noun, verb, adjective), and print the completed story.

What you learn: String formatting, file reading, regular expressions with re.

Time: 1–2 hours | Difficulty:

import re

template = """
The {adjective} {noun} decided to {verb} across the {place}.
Everyone was {emotion} to see it {verb2} so {adverb}.
"""

placeholders = re.findall(r"\{(\w+)\}", template)
words = {}
for word in placeholders:
    if word not in words:
        words[word] = input(f"Enter a(n) {word}: ")

print("\n--- Your Story ---")
print(template.format(**words))

#7 — Unit converter

What you build: Convert between units: length (km/miles/feet), weight (kg/lb), temperature (°C/°F/K). Menu-driven CLI.

What you learn: Nested functions, dictionaries as dispatch tables, arithmetic, input validation.

Time: 2–3 hours | Difficulty: ⭐⭐

def celsius_to(value, unit):
    if unit == "fahrenheit":
        return value * 9/5 + 32
    if unit == "kelvin":
        return value + 273.15
    raise ValueError("Unknown unit")

def km_to(value, unit):
    factors = {"miles": 0.621371, "feet": 3280.84, "meters": 1000}
    return value * factors[unit]

converters = {
    "temperature": celsius_to,
    "distance": km_to,
}

#8 — Countdown timer

What you build: A CLI countdown timer — user sets hours/minutes/seconds, then sees the countdown update in place. Plays a sound (or prints a message) when done.

What you learn: time.sleep, ANSI escape codes for in-place terminal updates, datetime.

Time: 2–3 hours | Difficulty: ⭐⭐

import time

def countdown(seconds):
    while seconds:
        h, rem = divmod(seconds, 3600)
        m, s = divmod(rem, 60)
        print(f"\r{h:02d}:{m:02d}:{s:02d}", end="", flush=True)
        time.sleep(1)
        seconds -= 1
    print("\r00:00:00 — Time's up! 🔔")

minutes = int(input("Minutes: "))
countdown(minutes * 60)

Intermediate projects (#9–16)

#9 — Weather app (API)

What you build: Fetch real weather data from the OpenWeatherMap API for any city and display temperature, humidity, wind speed, and a description.

What you learn: HTTP requests with requests, JSON parsing, API keys, environment variables.

Time: 3–5 hours | Difficulty: ⭐⭐

import os
import requests

API_KEY = os.environ["OPENWEATHER_API_KEY"]

def get_weather(city: str) -> dict:
    url = "https://api.openweathermap.org/data/2.5/weather"
    response = requests.get(url, params={
        "q": city,
        "appid": API_KEY,
        "units": "metric",
    })
    response.raise_for_status()
    data = response.json()
    return {
        "city": data["name"],
        "temp": data["main"]["temp"],
        "feels_like": data["main"]["feels_like"],
        "humidity": data["main"]["humidity"],
        "description": data["weather"][0]["description"],
        "wind": data["wind"]["speed"],
    }

weather = get_weather("London")
print(f"{weather['city']}: {weather['temp']}°C, {weather['description']}")
print(f"Humidity: {weather['humidity']}% | Wind: {weather['wind']} m/s")

Extensions: Add 5-day forecast, save history to CSV, build a Flask web UI.


#10 — Expense tracker

What you build: Track income and expenses, categorize them, and show monthly summaries with totals and charts using matplotlib.

What you learn: Classes, datetime, CSV files, matplotlib, data aggregation with collections.defaultdict.

Time: 4–6 hours | Difficulty: ⭐⭐

import csv
from datetime import datetime
from collections import defaultdict
from dataclasses import dataclass, field

@dataclass
class Transaction:
    date: str
    amount: float
    category: str
    description: str
    type: str  # "income" or "expense"

class ExpenseTracker:
    def __init__(self, filepath="transactions.csv"):
        self.filepath = filepath
        self.transactions: list[Transaction] = []
        self._load()

    def add(self, amount, category, description, type_="expense"):
        t = Transaction(
            date=datetime.now().strftime("%Y-%m-%d"),
            amount=amount,
            category=category,
            description=description,
            type=type_,
        )
        self.transactions.append(t)
        self._save()

    def summary(self):
        totals = defaultdict(float)
        for t in self.transactions:
            totals[t.category] += t.amount if t.type == "income" else -t.amount
        return dict(totals)

    def _save(self):
        with open(self.filepath, "w", newline="") as f:
            writer = csv.DictWriter(f, fieldnames=["date","amount","category","description","type"])
            writer.writeheader()
            for t in self.transactions:
                writer.writerow(vars(t))

    def _load(self):
        try:
            with open(self.filepath) as f:
                for row in csv.DictReader(f):
                    self.transactions.append(Transaction(**{
                        **row, "amount": float(row["amount"])
                    }))
        except FileNotFoundError:
            pass

#11 — Web scraper

What you build: Scrape a public website (e.g., Hacker News top stories, book prices from books.toscrape.com) and save results to CSV.

What you learn: requests, BeautifulSoup, HTML parsing, pagination, rate limiting.

Time: 4–8 hours | Difficulty: ⭐⭐

import csv
import time
import requests
from bs4 import BeautifulSoup

BASE_URL = "http://books.toscrape.com/catalogue/page-{}.html"

def scrape_books(pages=5):
    books = []
    for page in range(1, pages + 1):
        resp = requests.get(BASE_URL.format(page))
        soup = BeautifulSoup(resp.text, "html.parser")
        for article in soup.select("article.product_pod"):
            books.append({
                "title": article.h3.a["title"],
                "price": article.select_one(".price_color").text,
                "rating": article.p["class"][1],
            })
        time.sleep(0.5)  # be polite
    return books

books = scrape_books()
with open("books.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["title", "price", "rating"])
    writer.writeheader()
    writer.writerows(books)

print(f"Scraped {len(books)} books")

Extensions: Scrape multiple categories, filter by rating, add --pages CLI argument with argparse.


#12 — URL shortener

What you build: A Flask web app that accepts a long URL, stores it with a random 6-character code in SQLite, and redirects short URLs to the original.

What you learn: Flask, SQLite with sqlite3, URL routing, redirect, basic HTML forms.

Time: 4–6 hours | Difficulty: ⭐⭐⭐

import random
import string
import sqlite3
from flask import Flask, request, redirect, render_template_string

app = Flask(__name__)
DB = "urls.db"

def init_db():
    with sqlite3.connect(DB) as con:
        con.execute("CREATE TABLE IF NOT EXISTS urls (code TEXT PRIMARY KEY, url TEXT)")

def shorten(url):
    code = "".join(random.choices(string.ascii_letters + string.digits, k=6))
    with sqlite3.connect(DB) as con:
        con.execute("INSERT INTO urls VALUES (?, ?)", (code, url))
    return code

@app.route("/", methods=["GET", "POST"])
def index():
    short = None
    if request.method == "POST":
        url = request.form["url"]
        short = shorten(url)
    return render_template_string("""
        <form method=post><input name=url placeholder="Long URL">
        <button>Shorten</button></form>
        {% if short %}<p>Short: <a href="/{{ short }}">{{ request.host }}/{{ short }}</a>{% endif %}
    """, short=short)

@app.route("/<code>")
def redirect_url(code):
    with sqlite3.connect(DB) as con:
        row = con.execute("SELECT url FROM urls WHERE code=?", (code,)).fetchone()
    if row:
        return redirect(row[0])
    return "Not found", 404

if __name__ == "__main__":
    init_db()
    app.run(debug=True)

#13 — CSV data analyser

What you build: Load any CSV file, display summary statistics (mean, median, std dev per column), find missing values, and plot a correlation heatmap.

What you learn: pandas, matplotlib/seaborn, data cleaning, descriptive statistics.

Time: 4–8 hours | Difficulty: ⭐⭐

import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

def analyse(filepath: str):
    df = pd.read_csv(filepath)
    print(f"Shape: {df.shape}")
    print("\nMissing values:")
    print(df.isnull().sum()[df.isnull().sum() > 0])
    print("\nSummary statistics:")
    print(df.describe())

    numeric = df.select_dtypes(include="number")
    if len(numeric.columns) > 1:
        plt.figure(figsize=(10, 8))
        sns.heatmap(numeric.corr(), annot=True, cmap="coolwarm", fmt=".2f")
        plt.title("Correlation heatmap")
        plt.tight_layout()
        plt.show()

analyse("titanic.csv")

#14 — Flashcard quiz app

What you build: Load a deck of flashcards from a JSON file, quiz the user, track scores, and use spaced repetition to prioritize hard cards.

What you learn: JSON, OOP, algorithms (spaced repetition), random, state management.

Time: 4–6 hours | Difficulty: ⭐⭐

import json
import random
from dataclasses import dataclass, field

@dataclass
class Card:
    question: str
    answer: str
    score: int = 0  # higher = easier, show less often

def load_deck(path: str) -> list[Card]:
    with open(path) as f:
        return [Card(**c) for c in json.load(f)]

def quiz(deck: list[Card], rounds: int = 10):
    correct = 0
    for _ in range(rounds):
        # Weight cards by difficulty: lower score = shown more often
        weights = [1 / (c.score + 1) for c in deck]
        card = random.choices(deck, weights=weights, k=1)[0]
        print(f"\nQ: {card.question}")
        answer = input("A: ").strip()
        if answer.lower() == card.answer.lower():
            print("✓ Correct!")
            card.score += 1
            correct += 1
        else:
            print(f"✗ Answer: {card.answer}")
            card.score = max(0, card.score - 1)
    print(f"\nScore: {correct}/{rounds}")

#15 — Email sender automation

What you build: Send personalised bulk emails from a CSV of recipients using Python's smtplib and a Jinja2 template. Dry-run mode previews without sending.

What you learn: smtplib, email.mime, Jinja2 templates, CSV, environment variables for secrets.

Time: 3–5 hours | Difficulty: ⭐⭐

import csv
import os
import smtplib
from email.mime.text import MIMEText
from jinja2 import Template

TEMPLATE = Template("Hello {{ name }},\n\nThank you for signing up!")

def send_emails(recipients_csv: str, dry_run=True):
    with open(recipients_csv) as f:
        recipients = list(csv.DictReader(f))

    if dry_run:
        for r in recipients:
            print(f"[DRY RUN] To: {r['email']}")
            print(TEMPLATE.render(**r))
        return

    with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
        server.login(os.environ["EMAIL_USER"], os.environ["EMAIL_PASS"])
        for r in recipients:
            msg = MIMEText(TEMPLATE.render(**r))
            msg["Subject"] = "Welcome!"
            msg["From"] = os.environ["EMAIL_USER"]
            msg["To"] = r["email"]
            server.send_message(msg)
            print(f"Sent to {r['email']}")

#16 — CLI file organiser

What you build: Automatically sort files in a directory into subfolders by extension (Images/, Documents/, Videos/, etc.). Supports dry-run and undo via a manifest.

What you learn: pathlib, shutil, argparse, logging, writing revertible scripts.

Time: 3–5 hours | Difficulty: ⭐⭐

import json
import shutil
import argparse
from pathlib import Path

CATEGORIES = {
    "Images": [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"],
    "Documents": [".pdf", ".docx", ".doc", ".txt", ".xlsx", ".csv"],
    "Videos": [".mp4", ".mov", ".avi", ".mkv"],
    "Audio": [".mp3", ".wav", ".flac", ".aac"],
    "Archives": [".zip", ".tar", ".gz", ".rar"],
    "Code": [".py", ".js", ".ts", ".html", ".css", ".json"],
}

def organise(directory: Path, dry_run=False):
    ext_map = {ext: cat for cat, exts in CATEGORIES.items() for ext in exts}
    manifest = []
    for file in directory.iterdir():
        if file.is_dir():
            continue
        category = ext_map.get(file.suffix.lower(), "Other")
        dest_dir = directory / category
        dest = dest_dir / file.name
        print(f"{'[DRY] ' if dry_run else ''}Moving {file.name} → {category}/")
        if not dry_run:
            dest_dir.mkdir(exist_ok=True)
            shutil.move(str(file), str(dest))
            manifest.append({"from": str(dest), "to": str(file)})
    if not dry_run:
        (directory / ".organise-manifest.json").write_text(json.dumps(manifest, indent=2))

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("directory")
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args()
    organise(Path(args.directory), dry_run=args.dry_run)

Advanced projects (#17–25)

#17 — REST API with FastAPI

What you build: A fully functional REST API with CRUD endpoints for a notes app, JWT authentication, SQLAlchemy ORM, and auto-generated Swagger docs.

What you learn: FastAPI, Pydantic v2, SQLAlchemy, JWT, dependency injection, OpenAPI.

Time: 1–2 days | Difficulty: ⭐⭐⭐

from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker

engine = create_engine("sqlite:///notes.db")
SessionLocal = sessionmaker(bind=engine)
app = FastAPI()

class Base(DeclarativeBase):
    pass

class Note(Base):
    __tablename__ = "notes"
    id = Column(Integer, primary_key=True)
    title = Column(String, nullable=False)
    body = Column(String, default="")

Base.metadata.create_all(engine)

class NoteIn(BaseModel):
    title: str
    body: str = ""

class NoteOut(NoteIn):
    id: int
    class Config:
        from_attributes = True

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/notes", response_model=list[NoteOut])
def list_notes(db: Session = Depends(get_db)):
    return db.query(Note).all()

@app.post("/notes", response_model=NoteOut, status_code=201)
def create_note(note: NoteIn, db: Session = Depends(get_db)):
    db_note = Note(**note.model_dump())
    db.add(db_note)
    db.commit()
    db.refresh(db_note)
    return db_note

@app.delete("/notes/{note_id}", status_code=204)
def delete_note(note_id: int, db: Session = Depends(get_db)):
    note = db.get(Note, note_id)
    if not note:
        raise HTTPException(404, "Note not found")
    db.delete(note)
    db.commit()

#18 — Discord bot

What you build: A Discord bot with slash commands — poll creation, random meme fetching from Reddit API, moderation commands (kick/ban), and auto-role on join.

What you learn: discord.py, async/await, event-driven programming, slash commands, API integration.

Time: 1–2 days | Difficulty: ⭐⭐⭐

import discord
from discord import app_commands

intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)

@tree.command(name="roll", description="Roll a dice")
@app_commands.describe(sides="Number of sides (default 6)")
async def roll(interaction: discord.Interaction, sides: int = 6):
    import random
    result = random.randint(1, sides)
    await interaction.response.send_message(f"🎲 You rolled a **{result}** (d{sides})")

@client.event
async def on_ready():
    await tree.sync()
    print(f"Logged in as {client.user}")

client.run("YOUR_BOT_TOKEN")

#19 — Stock price tracker

What you build: Fetch real-time stock data from Yahoo Finance (yfinance), track a portfolio, show P&L, and send an email alert when a stock moves more than X%.

What you learn: yfinance, pandas, portfolio math, scheduled tasks with schedule, alerting.

Time: 1–2 days | Difficulty: ⭐⭐⭐

import yfinance as yf
import pandas as pd

def get_portfolio_value(portfolio: dict[str, int]) -> pd.DataFrame:
    """portfolio = {'AAPL': 10, 'MSFT': 5}"""
    rows = []
    for ticker, shares in portfolio.items():
        stock = yf.Ticker(ticker)
        price = stock.fast_info["last_price"]
        rows.append({
            "ticker": ticker,
            "shares": shares,
            "price": price,
            "value": price * shares,
        })
    df = pd.DataFrame(rows)
    df["weight"] = df["value"] / df["value"].sum() * 100
    return df

portfolio = {"AAPL": 10, "MSFT": 5, "GOOGL": 2}
df = get_portfolio_value(portfolio)
print(df.to_string(index=False))
print(f"\nTotal: ${df['value'].sum():,.2f}")

#20 — Image processing tool

What you build: A CLI tool to batch-process images — resize, convert format, add watermark, remove background, and generate thumbnails.

What you learn: Pillow, argparse, batch file processing, image manipulation.

Time: 1–2 days | Difficulty: ⭐⭐⭐

from pathlib import Path
from PIL import Image, ImageDraw, ImageFont

def process_images(input_dir: str, output_dir: str,
                   size=(800, 600), watermark="© MyBrand", quality=85):
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    for img_path in Path(input_dir).glob("*.{jpg,jpeg,png,webp}"):
        with Image.open(img_path) as img:
            img = img.convert("RGB")
            img.thumbnail(size)
            # Add watermark
            draw = ImageDraw.Draw(img)
            draw.text((10, img.height - 30), watermark, fill=(255, 255, 255, 128))
            out = Path(output_dir) / (img_path.stem + ".webp")
            img.save(out, "WEBP", quality=quality)
            print(f"Processed: {img_path.name} → {out.name}")

#21 — Markdown to PDF converter

What you build: Convert a Markdown file to a styled PDF using markdown + weasyprint. Supports custom CSS themes (dark/light) and a table of contents.

What you learn: markdown, weasyprint, CSS styling, template rendering with Jinja2.

Time: 4–8 hours | Difficulty: ⭐⭐⭐

import markdown
from weasyprint import HTML

def md_to_pdf(md_path: str, out_path: str, theme="light"):
    with open(md_path) as f:
        content = f.read()

    html_body = markdown.markdown(
        content, extensions=["tables", "fenced_code", "toc"]
    )

    styles = {
        "light": "body { font-family: Georgia; max-width: 800px; margin: auto; }",
        "dark": "body { font-family: Georgia; background:#1a1a1a; color:#eee; }",
    }

    html = f"""<!DOCTYPE html>
<html><head><style>{styles[theme]}</style></head>
<body>{html_body}</body></html>"""

    HTML(string=html).write_pdf(out_path)
    print(f"PDF saved to {out_path}")

md_to_pdf("README.md", "output.pdf")

#22 — Machine learning: spam detector

What you build: Train a Naive Bayes classifier on the SMS Spam Collection dataset to detect spam messages, with a Flask API for predictions.

What you learn: scikit-learn, TfidfVectorizer, train/test split, model evaluation, joblib serialisation.

Time: 1–2 days | Difficulty: ⭐⭐⭐⭐

import joblib
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

df = pd.read_csv("spam.csv", encoding="latin-1")[["v1", "v2"]]
df.columns = ["label", "text"]

X_train, X_test, y_train, y_test = train_test_split(
    df["text"], df["label"], test_size=0.2, random_state=42
)

model = Pipeline([
    ("tfidf", TfidfVectorizer(stop_words="english", max_features=5000)),
    ("clf", MultinomialNB()),
])

model.fit(X_train, y_train)
print(classification_report(y_test, model.predict(X_test)))
joblib.dump(model, "spam_model.pkl")

# Predict
print(model.predict(["Congratulations! You've won a free prize! Click here!"]))  # ['spam']

#23 — Async web scraper

What you build: Scrape 100+ URLs concurrently using asyncio + aiohttp, parse results with BeautifulSoup, and save to PostgreSQL with asyncpg.

What you learn: asyncio, aiohttp, concurrency patterns, connection pooling, async context managers.

Time: 1–2 days | Difficulty: ⭐⭐⭐⭐

import asyncio
import aiohttp
from bs4 import BeautifulSoup

async def fetch(session: aiohttp.ClientSession, url: str) -> dict:
    try:
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
            html = await resp.text()
            soup = BeautifulSoup(html, "html.parser")
            return {"url": url, "title": soup.title.string if soup.title else "", "status": resp.status}
    except Exception as e:
        return {"url": url, "title": "", "status": 0, "error": str(e)}

async def scrape_all(urls: list[str], concurrency=20) -> list[dict]:
    sem = asyncio.Semaphore(concurrency)
    async with aiohttp.ClientSession() as session:
        async def bounded_fetch(url):
            async with sem:
                return await fetch(session, url)
        return await asyncio.gather(*[bounded_fetch(url) for url in urls])

urls = [f"https://example.com/page/{i}" for i in range(100)]
results = asyncio.run(scrape_all(urls))
print(f"Scraped {len(results)} pages")

#24 — Voice assistant

What you build: A voice assistant that wakes on a keyword, transcribes speech with speech_recognition, answers questions via the OpenAI API, and speaks back with pyttsx3.

What you learn: speech_recognition, pyttsx3, OpenAI API, threading, voice UX design.

Time: 1–2 days | Difficulty: ⭐⭐⭐⭐

import os
import speech_recognition as sr
import pyttsx3
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
engine = pyttsx3.init()

def speak(text: str):
    engine.say(text)
    engine.runAndWait()

def listen() -> str:
    r = sr.Recognizer()
    with sr.Microphone() as source:
        print("Listening...")
        audio = r.listen(source, timeout=5)
    return r.recognize_google(audio)

def ask_ai(prompt: str) -> str:
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
    )
    return resp.choices[0].message.content

while True:
    text = listen()
    print(f"You: {text}")
    if "exit" in text.lower():
        break
    reply = ask_ai(text)
    print(f"AI: {reply}")
    speak(reply)

#25 — Full-stack task manager (React + FastAPI)

What you build: A complete full-stack app — FastAPI backend with SQLAlchemy + PostgreSQL, React frontend with TypeScript, JWT auth, drag-and-drop Kanban board, and Docker Compose deployment.

What you learn: Full-stack architecture, API design, React state management, Docker, CORS, authentication flow.

Tech stack: FastAPI · SQLAlchemy · PostgreSQL · React · TypeScript · Tailwind · Docker Compose

This is the capstone project. Structure:

project/
├── backend/
│   ├── main.py          # FastAPI app
│   ├── models.py        # SQLAlchemy models
│   ├── schemas.py       # Pydantic schemas
│   ├── auth.py          # JWT helpers
│   └── requirements.txt
├── frontend/
│   ├── src/
│   │   ├── App.tsx
│   │   ├── api/         # API client
│   │   └── components/  # Board, Card, Auth
│   └── package.json
└── docker-compose.yml

Time: 1–2 weeks | Difficulty: ⭐⭐⭐⭐⭐


Project difficulty summary

# Project Difficulty Time Key library
1 Number guessing game 1–2 h random
2 To-do list (CLI) 2–3 h json
3 Calculator 1–2 h built-in
4 Password generator 1–2 h string
5 Rock-paper-scissors 1–2 h random
6 Mad libs 1–2 h re
7 Unit converter ⭐⭐ 2–3 h built-in
8 Countdown timer ⭐⭐ 2–3 h time
9 Weather app ⭐⭐ 3–5 h requests
10 Expense tracker ⭐⭐ 4–6 h pandas
11 Web scraper ⭐⭐ 4–8 h beautifulsoup4
12 URL shortener ⭐⭐⭐ 4–6 h flask
13 CSV analyser ⭐⭐ 4–8 h pandas, seaborn
14 Flashcard quiz ⭐⭐ 4–6 h built-in
15 Email automation ⭐⭐ 3–5 h smtplib
16 File organiser ⭐⭐ 3–5 h pathlib
17 REST API (FastAPI) ⭐⭐⭐ 1–2 d fastapi
18 Discord bot ⭐⭐⭐ 1–2 d discord.py
19 Stock tracker ⭐⭐⭐ 1–2 d yfinance
20 Image processing ⭐⭐⭐ 1–2 d Pillow
21 Markdown to PDF ⭐⭐⭐ 4–8 h weasyprint
22 Spam detector (ML) ⭐⭐⭐⭐ 1–2 d scikit-learn
23 Async scraper ⭐⭐⭐⭐ 1–2 d aiohttp
24 Voice assistant ⭐⭐⭐⭐ 1–2 d openai
25 Full-stack app ⭐⭐⭐⭐⭐ 1–2 w FastAPI + React

How to pick your first project

If you want to… Start with
Build confidence fast #1, #3, #5
Learn file handling #2, #14, #16
Work with the internet #9, #11, #18
Understand data #10, #13, #22
Build a web backend #12, #17
Impress employers #17, #22, #25
Automate daily tasks #15, #16, #9

Portfolio building tips

1. Put everything on GitHub

Create a public repo for each project. Write a clear README with:

  • What it does (2–3 sentences)
  • Screenshot or GIF
  • Installation instructions (pip install -r requirements.txt)
  • Usage examples

2. Document your code

Add docstrings to every function. Not because you'll forget — because employers read your code to see how you think.

3. Handle errors properly

A project that crashes on bad input looks worse than no project at all. Always validate input, catch exceptions, and give useful error messages.

4. Use virtual environments

python -m venv venv
source venv/bin/activate   # Windows: venv\Scripts\activate
pip install -r requirements.txt

Never commit node_modules or venv/ — always include a requirements.txt.

5. Deploy something

Static projects: GitHub Pages. Flask/FastAPI: Railway, Render, or Fly.io (all have free tiers). Having a live URL is 10× more impressive than a repo alone.


Common mistakes to avoid

Mistake Why it hurts Fix
Copying tutorials verbatim No learning, no portfolio value Modify, extend, break, rebuild
Starting too advanced Discouragement kills momentum Start with #1–4, work up
No error handling App crashes on first real use Always wrap external calls in try/except
Hard-coded secrets Security risk, bad habit Use os.environ or .env + python-dotenv
No README Nobody can use your project Write it as you go
One huge file Hard to read, hard to debug Split into modules early
Not using type hints Harder to maintain Add def greet(name: str) -> str everywhere
Skipping tests Bugs pile up silently Write at least one pytest test per function

Frequently asked questions

How many projects do I need for a job? 3–5 polished, deployed projects beat 20 half-finished ones. Employers look for depth and completeness, not quantity.

Which projects are most impressive to employers? Anything that solves a real problem, has a live URL, and shows you understand web fundamentals (#12, #17, #25). ML projects (#22) impress for data roles. APIs (#17) impress for backend roles.

Should I use Django or Flask or FastAPI? For beginners: Flask (simpler, less magic). For APIs: FastAPI (modern, typed, fast). For full-featured web apps: Django. Start with Flask (#12), then learn FastAPI (#17).

What Python version should I use? Python 3.12+ for new projects. Use match statements, tomllib, improved error messages, and f-string improvements. Install via pyenv to manage versions.

Can I build these projects without prior programming experience? Projects #1–6 require only the basics (variables, loops, functions — roughly 10–20 hours of learning). Work through a free resource like the official Python tutorial first, then build.

How do I get unstuck when I hit a bug?

  1. Read the error message — the last line tells you exactly what failed. 2. Add print() statements to see variable values. 3. Google the exact error message + "python". 4. Ask on Stack Overflow or in the Python Discord. Never spend more than 30 minutes stuck without asking for help.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools