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?
- 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.