March 27, 202624 min read

10 Python Projects for Beginners (With Full Code)

Build 10 real Python projects from scratch -- password generator, quiz game, expense tracker, web scraper, and more. Full code included for each one.

python projects beginners practice tutorial
Ad 336x280

Reading tutorials is not the same as writing code. You can read about Python for months and still freeze when you open an empty file. The only way past that is to build things.

Here are 10 projects, ordered from simplest to most complex. Each one introduces new concepts while reinforcing the ones before it. Every project includes complete, working code that you can type out, run, and then modify.

Don't just copy-paste. Type the code yourself. Modify it. Break it. Fix it. That's where the learning happens.

Project 1: Password Generator

Concepts: imports, strings, random, functions, user input

A command-line tool that generates random passwords of a specified length.

import random
import string

def generate_password(length, use_special=True):
characters = string.ascii_letters + string.digits
if use_special:
characters += string.punctuation

password = ''.join(random.choice(characters) for _ in range(length))
return password

def main():
print("--- Password Generator ---")

while True:
try:
length = int(input("\nPassword length (8-128): "))
if length < 8 or length > 128:
print("Please choose a length between 8 and 128.")
continue
break
except ValueError:
print("Please enter a valid number.")

include_special = input("Include special characters? (y/n): ").lower().strip()
use_special = include_special != 'n'

print(f"\nYour password: {generate_password(length, use_special)}")

# Generate a few more options print("\nMore options:") for i in range(3): print(f" {i + 1}. {generate_password(length, use_special)}")

if __name__ == "__main__":
main()

What you learn: How to use Python's standard library (random, string), how to handle user input with error checking, and how if __name__ == "__main__" works. This is the pattern you'll use in every Python script. Extend it: Add options for minimum numbers of uppercase, lowercase, digits, and special characters. Add a "pronounceable password" mode.

Project 2: Quiz Game

Concepts: dictionaries, lists, loops, scoring logic, f-strings

An interactive multiple-choice quiz that tracks your score.

import random

QUESTIONS = [
{
"question": "What is the capital of Japan?",
"options": ["Seoul", "Tokyo", "Beijing", "Bangkok"],
"answer": "Tokyo"
},
{
"question": "Which planet is known as the Red Planet?",
"options": ["Venus", "Jupiter", "Mars", "Saturn"],
"answer": "Mars"
},
{
"question": "What year did the first iPhone release?",
"options": ["2005", "2006", "2007", "2008"],
"answer": "2007"
},
{
"question": "Which language is primarily used for web browsers?",
"options": ["Python", "JavaScript", "C++", "Java"],
"answer": "JavaScript"
},
{
"question": "What does 'HTTP' stand for?",
"options": [
"HyperText Transfer Protocol",
"High Tech Transfer Process",
"HyperText Transmission Program",
"High Transfer Text Protocol"
],
"answer": "HyperText Transfer Protocol"
},
{
"question": "Which data structure uses LIFO (Last In, First Out)?",
"options": ["Queue", "Array", "Stack", "Tree"],
"answer": "Stack"
},
{
"question": "What is the time complexity of binary search?",
"options": ["O(n)", "O(log n)", "O(n^2)", "O(1)"],
"answer": "O(log n)"
}
]

def run_quiz(questions):
random.shuffle(questions)
score = 0
total = len(questions)

print("=== Quiz Game ===\n")

for i, q in enumerate(questions, 1):
print(f"Question {i}/{total}: {q['question']}")

# Shuffle options for each question options = q["options"][:] random.shuffle(options)

for j, option in enumerate(options, 1):
print(f" {j}. {option}")

while True:
try:
choice = int(input("\nYour answer (number): "))
if 1 <= choice <= len(options):
break
print(f"Please enter a number between 1 and {len(options)}.")
except ValueError:
print("Please enter a valid number.")

selected = options[choice - 1]

if selected == q["answer"]:
print("Correct!\n")
score += 1
else:
print(f"Wrong! The answer was: {q['answer']}\n")

# Results percentage = (score / total) * 100 print("=== Results ===") print(f"Score: {score}/{total} ({percentage:.0f}%)")

if percentage == 100:
print("Perfect score!")
elif percentage >= 70:
print("Great job!")
elif percentage >= 50:
print("Not bad, keep practicing!")
else:
print("Keep studying, you'll get there!")

if __name__ == "__main__":
run_quiz(QUESTIONS)

What you learn: Working with lists of dictionaries (a very common data pattern), shuffling, enumeration, input validation, and basic game logic. Extend it: Load questions from a JSON file. Add difficulty levels. Track high scores across sessions by writing to a file.

Project 3: Expense Tracker

Concepts: file I/O, CSV, dates, data processing, command-line menus

A CLI expense tracker that saves data to a CSV file.

import csv
import os
from datetime import datetime

FILENAME = "expenses.csv"
CATEGORIES = ["food", "transport", "entertainment", "utilities", "shopping", "other"]

def load_expenses():
expenses = []
if os.path.exists(FILENAME):
with open(FILENAME, "r", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
row["amount"] = float(row["amount"])
expenses.append(row)
return expenses

def save_expenses(expenses):
with open(FILENAME, "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["date", "category", "description", "amount"])
writer.writeheader()
writer.writerows(expenses)

def add_expense(expenses):
print("\n--- Add Expense ---")

print("Categories:")
for i, cat in enumerate(CATEGORIES, 1):
print(f" {i}. {cat}")

while True:
try:
cat_choice = int(input("Category (number): "))
if 1 <= cat_choice <= len(CATEGORIES):
break
print(f"Enter a number between 1 and {len(CATEGORIES)}.")
except ValueError:
print("Enter a valid number.")

description = input("Description: ").strip()
if not description:
description = "No description"

while True:
try:
amount = float(input("Amount: $"))
if amount > 0:
break
print("Amount must be positive.")
except ValueError:
print("Enter a valid number.")

expense = {
"date": datetime.now().strftime("%Y-%m-%d"),
"category": CATEGORIES[cat_choice - 1],
"description": description,
"amount": amount
}

expenses.append(expense)
save_expenses(expenses)
print(f"Added: ${amount:.2f} for {description} ({CATEGORIES[cat_choice - 1]})")

def view_expenses(expenses):
if not expenses:
print("\nNo expenses recorded yet.")
return

print(f"\n{'Date':<12} {'Category':<15} {'Description':<25} {'Amount':>10}")
print("-" * 65)

for exp in expenses[-20:]: # Show last 20
print(f"{exp['date']:<12} {exp['category']:<15} {exp['description']:<25} ${exp['amount']:>9.2f}")

total = sum(exp["amount"] for exp in expenses)
print("-" * 65)
print(f"{'Total':<52} ${total:>9.2f}")

def view_summary(expenses):
if not expenses:
print("\nNo expenses to summarize.")
return

print("\n--- Expense Summary ---")

# By category category_totals = {} for exp in expenses: cat = exp["category"] category_totals[cat] = category_totals.get(cat, 0) + exp["amount"]

total = sum(category_totals.values())

print(f"\n{'Category':<15} {'Total':>10} {'Percentage':>12}")
print("-" * 40)

for cat, amount in sorted(category_totals.items(), key=lambda x: x[1], reverse=True):
pct = (amount / total) * 100
print(f"{cat:<15} ${amount:>9.2f} {pct:>10.1f}%")

print("-" * 40)
print(f"{'Total':<15} ${total:>9.2f}")
print(f"\nTotal expenses: {len(expenses)}")
print(f"Average expense: ${total / len(expenses):.2f}")

def delete_expense(expenses):
if not expenses:
print("\nNo expenses to delete.")
return

# Show recent expenses with indices print("\nRecent expenses:") recent = expenses[-10:] start_idx = len(expenses) - len(recent)

for i, exp in enumerate(recent):
actual_idx = start_idx + i
print(f" {actual_idx + 1}. [{exp['date']}] {exp['description']} - ${exp['amount']:.2f}")

try:
choice = int(input("\nDelete expense number (0 to cancel): "))
if choice == 0:
return
if 1 <= choice <= len(expenses):
removed = expenses.pop(choice - 1)
save_expenses(expenses)
print(f"Deleted: {removed['description']} (${removed['amount']:.2f})")
else:
print("Invalid number.")
except ValueError:
print("Enter a valid number.")

def main():
expenses = load_expenses()

while True:
print("\n=== Expense Tracker ===")
print("1. Add expense")
print("2. View expenses")
print("3. View summary")
print("4. Delete expense")
print("5. Exit")

choice = input("\nChoice: ").strip()

if choice == "1":
add_expense(expenses)
elif choice == "2":
view_expenses(expenses)
elif choice == "3":
view_summary(expenses)
elif choice == "4":
delete_expense(expenses)
elif choice == "5":
print("Goodbye!")
break
else:
print("Invalid choice. Enter 1-5.")

if __name__ == "__main__":
main()

What you learn: File I/O with CSV, data persistence, building a menu-driven application, data aggregation and analysis, string formatting for aligned output. Extend it: Add monthly breakdowns. Add a budget feature that warns when you're over budget. Export to different formats.

Project 4: Web Scraper

Concepts: HTTP requests, HTML parsing, external libraries, data extraction

A scraper that pulls the top stories from Hacker News.

First, install the required libraries:

pip install requests beautifulsoup4
import requests
from bs4 import BeautifulSoup

def scrape_hacker_news(num_stories=10):
url = "https://news.ycombinator.com/"
headers = {"User-Agent": "Mozilla/5.0 (educational scraper)"}

response = requests.get(url, headers=headers)
response.raise_for_status()

soup = BeautifulSoup(response.text, "html.parser")

stories = []
title_links = soup.select(".titleline > a")
score_elements = soup.select(".score")
subtext_elements = soup.select(".subtext")

for i in range(min(num_stories, len(title_links))):
title = title_links[i].get_text()
link = title_links[i].get("href", "")

# Handle relative links if link.startswith("item?"): link = f"https://news.ycombinator.com/{link}" # Get score if available score = 0 if i < len(score_elements): score_text = score_elements[i].get_text() score = int(score_text.split()[0]) # Get comment count comments = 0 if i < len(subtext_elements): links = subtext_elements[i].find_all("a") for a in links: if "comment" in a.get_text().lower(): comment_text = a.get_text().split()[0] try: comments = int(comment_text) except ValueError: comments = 0

stories.append({
"rank": i + 1,
"title": title,
"link": link,
"score": score,
"comments": comments
})

return stories

def display_stories(stories):
print("=== Hacker News Top Stories ===\n")

for story in stories:
print(f"{story['rank']:>2}. {story['title']}")
print(f" Points: {story['score']} | Comments: {story['comments']}")
print(f" {story['link']}")
print()

def save_to_file(stories, filename="hacker_news.txt"):
with open(filename, "w", encoding="utf-8") as f:
f.write("Hacker News Top Stories\n")
f.write("=" * 50 + "\n\n")

for story in stories:
f.write(f"{story['rank']}. {story['title']}\n")
f.write(f" Points: {story['score']} | Comments: {story['comments']}\n")
f.write(f" {story['link']}\n\n")

print(f"Saved {len(stories)} stories to {filename}")

if __name__ == "__main__":
print("Fetching top stories from Hacker News...\n")

try:
stories = scrape_hacker_news(15)
display_stories(stories)

save = input("Save to file? (y/n): ").lower().strip()
if save == "y":
save_to_file(stories)

except requests.RequestException as e:
print(f"Error fetching page: {e}")
except Exception as e:
print(f"Error parsing page: {e}")

What you learn: Making HTTP requests, parsing HTML, CSS selectors, error handling for network operations, and working with external libraries. Extend it: Scrape multiple pages. Add filtering by minimum score. Schedule it to run daily and save results. Be respectful -- always check a site's robots.txt and don't make too many requests.

Project 5: Weather CLI

Concepts: APIs, JSON, environment variables, error handling

A command-line weather app that uses a free weather API.

pip install requests python-dotenv

Sign up for a free API key at openweathermap.org, then create a .env file:

WEATHER_API_KEY=your_api_key_here
import os
import sys
import requests
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.getenv("WEATHER_API_KEY")
BASE_URL = "https://api.openweathermap.org/data/2.5"

def get_weather(city):
if not API_KEY:
print("Error: WEATHER_API_KEY not set in .env file")
sys.exit(1)

params = {
"q": city,
"appid": API_KEY,
"units": "metric"
}

response = requests.get(f"{BASE_URL}/weather", params=params)

if response.status_code == 404:
return None
response.raise_for_status()

return response.json()

def get_forecast(city):
params = {
"q": city,
"appid": API_KEY,
"units": "metric",
"cnt": 8 # Next 24 hours (3-hour intervals)
}

response = requests.get(f"{BASE_URL}/forecast", params=params)
response.raise_for_status()

return response.json()

def display_weather(data):
name = data["name"]
country = data["sys"]["country"]
temp = data["main"]["temp"]
feels_like = data["main"]["feels_like"]
humidity = data["main"]["humidity"]
description = data["weather"][0]["description"]
wind_speed = data["wind"]["speed"]

print(f"\n--- Weather in {name}, {country} ---")
print(f"Condition: {description.title()}")
print(f"Temperature: {temp:.1f} C (feels like {feels_like:.1f} C)")
print(f"Humidity: {humidity}%")
print(f"Wind: {wind_speed} m/s")

def display_forecast(data):
print(f"\n--- 24-Hour Forecast ---")
print(f"{'Time':<18} {'Temp':>6} {'Condition':<20}")
print("-" * 48)

for item in data["list"]:
dt = item["dt_txt"]
temp = item["main"]["temp"]
desc = item["weather"][0]["description"]
print(f"{dt:<18} {temp:>5.1f}C {desc}")

def main():
print("=== Weather CLI ===")

while True:
city = input("\nEnter city name (or 'quit'): ").strip()

if city.lower() in ("quit", "q", "exit"):
print("Goodbye!")
break

if not city:
print("Please enter a city name.")
continue

try:
weather = get_weather(city)

if weather is None:
print(f"City '{city}' not found. Check the spelling.")
continue

display_weather(weather)

show_forecast = input("\nShow 24h forecast? (y/n): ").lower().strip()
if show_forecast == "y":
forecast = get_forecast(city)
display_forecast(forecast)

except requests.RequestException as e:
print(f"Error fetching weather data: {e}")

if __name__ == "__main__":
main()

What you learn: Working with REST APIs, parsing JSON responses, using environment variables to keep secrets out of code, and building interactive CLI applications. Extend it: Add support for coordinates. Cache results to avoid redundant API calls. Add a "compare cities" feature.

Project 6: Todo List (with JSON Persistence)

Concepts: JSON file storage, CRUD operations, data validation, status management
import json
import os
from datetime import datetime

TODO_FILE = "todos.json"

def load_todos():
if os.path.exists(TODO_FILE):
with open(TODO_FILE, "r") as f:
return json.load(f)
return []

def save_todos(todos):
with open(TODO_FILE, "w") as f:
json.dump(todos, f, indent=2)

def add_todo(todos):
text = input("What needs to be done? ").strip()
if not text:
print("Todo cannot be empty.")
return

priority = input("Priority (high/medium/low) [medium]: ").strip().lower()
if priority not in ("high", "medium", "low"):
priority = "medium"

todo = {
"id": max((t["id"] for t in todos), default=0) + 1,
"text": text,
"done": False,
"priority": priority,
"created": datetime.now().strftime("%Y-%m-%d %H:%M"),
"completed": None
}

todos.append(todo)
save_todos(todos)
print(f"Added: '{text}' [{priority}]")

def list_todos(todos, show_done=False):
filtered = todos if show_done else [t for t in todos if not t["done"]]

if not filtered:
print("\nNo todos to show." if not show_done else "\nNo todos at all.")
return

# Sort by priority, then by date priority_order = {"high": 0, "medium": 1, "low": 2} filtered.sort(key=lambda t: (t["done"], priority_order.get(t["priority"], 1)))

print(f"\n{'ID':<5} {'Status':<8} {'Priority':<10} {'Todo':<35} {'Created':<12}")
print("-" * 72)

for todo in filtered:
status = "[x]" if todo["done"] else "[ ]"
pri = todo["priority"]
marker = "!" if pri == "high" else " "

print(f"{todo['id']:<5} {status:<8} {marker}{pri:<9} {todo['text']:<35} {todo['created']:<12}")

total = len(todos)
done = sum(1 for t in todos if t["done"])
print(f"\n{done}/{total} completed")

def complete_todo(todos):
list_todos(todos)

try:
todo_id = int(input("\nComplete todo ID (0 to cancel): "))
if todo_id == 0:
return

for todo in todos:
if todo["id"] == todo_id:
if todo["done"]:
print("Already completed.")
return
todo["done"] = True
todo["completed"] = datetime.now().strftime("%Y-%m-%d %H:%M")
save_todos(todos)
print(f"Completed: '{todo['text']}'")
return

print("Todo not found.")
except ValueError:
print("Enter a valid number.")

def delete_todo(todos):
list_todos(todos, show_done=True)

try:
todo_id = int(input("\nDelete todo ID (0 to cancel): "))
if todo_id == 0:
return

for i, todo in enumerate(todos):
if todo["id"] == todo_id:
removed = todos.pop(i)
save_todos(todos)
print(f"Deleted: '{removed['text']}'")
return

print("Todo not found.")
except ValueError:
print("Enter a valid number.")

def main():
todos = load_todos()

while True:
print("\n=== Todo List ===")
print("1. Add todo")
print("2. View active todos")
print("3. View all todos")
print("4. Complete todo")
print("5. Delete todo")
print("6. Exit")

choice = input("\nChoice: ").strip()

if choice == "1":
add_todo(todos)
elif choice == "2":
list_todos(todos)
elif choice == "3":
list_todos(todos, show_done=True)
elif choice == "4":
complete_todo(todos)
elif choice == "5":
delete_todo(todos)
elif choice == "6":
print("Goodbye!")
break
else:
print("Invalid choice.")

if __name__ == "__main__":
main()

What you learn: JSON for structured data persistence, CRUD patterns (Create, Read, Update, Delete) that apply to every application, sorting with custom keys, and building a complete data management application. Extend it: Add due dates with overdue warnings. Add search/filter functionality. Export to markdown format.

Project 7: File Organizer

Concepts: os and pathlib, file system operations, pattern matching, automation

A script that organizes messy directories by sorting files into folders by type.

import os
import shutil
from pathlib import Path
from collections import defaultdict

# File type categories
FILE_CATEGORIES = {
    "Images": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".ico"],
    "Documents": [".pdf", ".doc", ".docx", ".txt", ".rtf", ".odt", ".xls", ".xlsx", ".ppt", ".pptx"],
    "Videos": [".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm"],
    "Audio": [".mp3", ".wav", ".flac", ".aac", ".ogg", ".wma"],
    "Archives": [".zip", ".rar", ".7z", ".tar", ".gz", ".bz2"],
    "Code": [".py", ".js", ".ts", ".html", ".css", ".java", ".cpp", ".c", ".go", ".rs"],
    "Data": [".json", ".csv", ".xml", ".yaml", ".yml", ".sql", ".db"],
}

def get_category(extension):
ext = extension.lower()
for category, extensions in FILE_CATEGORIES.items():
if ext in extensions:
return category
return "Other"

def preview_organization(source_dir):
"""Show what would happen without actually moving files."""
source = Path(source_dir)

if not source.exists():
print(f"Directory not found: {source_dir}")
return None

moves = defaultdict(list)

for item in source.iterdir():
if item.is_file() and not item.name.startswith("."):
category = get_category(item.suffix)
moves[category].append(item.name)

if not moves:
print("No files to organize.")
return None

print(f"\n--- Preview: Organizing {source_dir} ---\n")

total = 0
for category in sorted(moves.keys()):
files = moves[category]
total += len(files)
print(f"{category}/ ({len(files)} files)")
for f in sorted(files)[:5]:
print(f" -> {f}")
if len(files) > 5:
print(f" ... and {len(files) - 5} more")
print()

print(f"Total: {total} files into {len(moves)} folders")
return moves

def organize_files(source_dir, dry_run=False):
source = Path(source_dir)
moved = 0
errors = 0

for item in source.iterdir():
if item.is_file() and not item.name.startswith("."):
category = get_category(item.suffix)
target_dir = source / category

if not dry_run:
target_dir.mkdir(exist_ok=True)
target_path = target_dir / item.name

# Handle duplicate filenames if target_path.exists(): stem = item.stem suffix = item.suffix counter = 1 while target_path.exists(): target_path = target_dir / f"{stem}_{counter}{suffix}" counter += 1

try:
shutil.move(str(item), str(target_path))
moved += 1
except Exception as e:
print(f"Error moving {item.name}: {e}")
errors += 1
else:
moved += 1

return moved, errors

def main():
print("=== File Organizer ===\n")

source_dir = input("Directory to organize (or '.' for current): ").strip()
if not source_dir:
source_dir = "."

source_dir = os.path.expanduser(source_dir)

# Preview first moves = preview_organization(source_dir)

if moves is None:
return

confirm = input("\nProceed with organization? (y/n): ").lower().strip()

if confirm == "y":
moved, errors = organize_files(source_dir)
print(f"\nDone! Moved {moved} files. Errors: {errors}")
else:
print("Cancelled.")

if __name__ == "__main__":
main()

What you learn: File system operations with pathlib (the modern way to handle paths in Python), shutil for moving files, defaultdict for grouping data, and the preview-then-execute pattern that prevents accidental data loss. Extend it: Add an undo feature that logs moves and can reverse them. Add date-based organization (by month/year). Add recursive organization for nested directories.

Project 8: Markdown to HTML Converter

Concepts: string processing, regular expressions, file I/O, text transformation
import re
import sys
import os

def convert_markdown_to_html(markdown_text):
lines = markdown_text.split("\n")
html_lines = []
in_code_block = False
in_list = False
in_paragraph = False

for line in lines:
# Code blocks
if line.strip().startswith("

"):
if in_code_block:
html_lines.append("")
in_code_block = False
else:
lang = line.strip()[3:].strip()
if lang:
html_lines.append(f'
')
else:
html_lines.append("
")
in_code_block = True
continue

if in_code_block:
# Escape HTML inside code blocks
escaped = line.replace("&", "&").replace("<", "<").replace(">", ">")
html_lines.append(escaped)
continue

# Headings heading_match = re.match(r"^(#{1,6})\s+(.+)$", line) if heading_match: close_blocks(html_lines, in_list, in_paragraph) in_list = False in_paragraph = False level = len(heading_match.group(1)) text = process_inline(heading_match.group(2)) html_lines.append(f"{text}") continue # Horizontal rule if re.match(r"^(-{3,}|\*{3,}|_{3,})$", line.strip()): close_blocks(html_lines, in_list, in_paragraph) in_list = False in_paragraph = False html_lines.append("
") continue # Unordered list items list_match = re.match(r"^[\*\-\+]\s+(.+)$", line.strip()) if list_match: if in_paragraph: html_lines.append("

") in_paragraph = False if not in_list: html_lines.append("
    ") in_list = True text = process_inline(list_match.group(1)) html_lines.append(f"
  • {text}
  • ") continue # Close list if we're no longer in one if in_list and not list_match: html_lines.append("
") in_list = False # Blockquote quote_match = re.match(r"^>\s(.)$", line) if quote_match: if in_paragraph: html_lines.append("

") in_paragraph = False text = process_inline(quote_match.group(1)) html_lines.append(f"

{text}

") continue # Empty line if line.strip() == "": if in_paragraph: html_lines.append("

") in_paragraph = False html_lines.append("") continue # Regular paragraph text if not in_paragraph: html_lines.append("

") in_paragraph = True

html_lines.append(process_inline(line))

# Close any open blocks if in_paragraph: html_lines.append("

") if in_list: html_lines.append("") if in_code_block: html_lines.append("
")

return "\n".join(html_lines)

def close_blocks(html_lines, in_list, in_paragraph):
if in_paragraph:
html_lines.append("

")
if in_list:
html_lines.append("")

def process_inline(text):
# Bold
text = re.sub(r"\\(.+?)\\", r"\1", text)
text = re.sub(r"__(.+?)__", r"\1", text)

# Italic text = re.sub(r"\(.+?)\", r"\1", text) text = re.sub(r"_(.+?)_", r"\1", text) # Inline code text = re.sub(r"(.+?)", r"\1", text) # Links text = re.sub(r"\[(.+?)\]\((.+?)\)", r'\1', text) # Images text = re.sub(r"!\[(.+?)\]\((.+?)\)", r'\1', text)

return text

def wrap_html(body, title="Document"):
return f"""




{title}



{body}

"""

def main():
if len(sys.argv) < 2:
print("Usage: python markdown_converter.py [output.html]")
print("\nOr enter markdown directly (Ctrl+D to finish):")

try:
text = sys.stdin.read()
except KeyboardInterrupt:
print("\nCancelled.")
return

html = convert_markdown_to_html(text)
print("\n--- HTML Output ---\n")
print(html)
return

input_file = sys.argv[1]

if not os.path.exists(input_file):
print(f"File not found: {input_file}")
return

output_file = sys.argv[2] if len(sys.argv) > 2 else input_file.rsplit(".", 1)[0] + ".html"

with open(input_file, "r", encoding="utf-8") as f:
markdown_text = f.read()

html_body = convert_markdown_to_html(markdown_text)
title = os.path.basename(input_file).rsplit(".", 1)[0].replace("-", " ").title()
full_html = wrap_html(html_body, title)

with open(output_file, "w", encoding="utf-8") as f:
f.write(full_html)

print(f"Converted: {input_file} -> {output_file}")

if __name__ == "__main__":
main()


What you learn: Regular expressions for text processing, state machines (tracking whether you're inside a code block, list, etc.), building a pipeline that transforms one format into another, and command-line argument handling.

Extend it: Add support for ordered lists, tables, and footnotes. Add syntax highlighting for code blocks. Build a live preview mode that watches for file changes.

Project 9: Simple REST API

Concepts: HTTP servers, routing, JSON APIs, CRUD operations

Build your own API server using Flask.

bash
pip install flask
python
from flask import Flask, request, jsonify
from datetime import datetime

app = Flask(__name__)

# In-memory database notes = {} next_id = 1

@app.route("/api/notes", methods=["GET"])
def get_notes():
tag = request.args.get("tag")

if tag:
filtered = {k: v for k, v in notes.items() if tag in v.get("tags", [])}
return jsonify(list(filtered.values()))

return jsonify(list(notes.values()))

@app.route("/api/notes/", methods=["GET"])
def get_note(note_id):
note = notes.get(note_id)
if not note:
return jsonify({"error": "Note not found"}), 404
return jsonify(note)

@app.route("/api/notes", methods=["POST"])
def create_note():
global next_id
data = request.get_json()

if not data or "title" not in data:
return jsonify({"error": "Title is required"}), 400

note = {
"id": next_id,
"title": data["title"],
"content": data.get("content", ""),
"tags": data.get("tags", []),
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat()
}

notes[next_id] = note
next_id += 1

return jsonify(note), 201

@app.route("/api/notes/", methods=["PUT"])
def update_note(note_id):
note = notes.get(note_id)
if not note:
return jsonify({"error": "Note not found"}), 404

data = request.get_json()

if "title" in data:
note["title"] = data["title"]
if "content" in data:
note["content"] = data["content"]
if "tags" in data:
note["tags"] = data["tags"]

note["updated_at"] = datetime.now().isoformat()

return jsonify(note)

@app.route("/api/notes/", methods=["DELETE"])
def delete_note(note_id):
if note_id not in notes:
return jsonify({"error": "Note not found"}), 404

del notes[note_id]
return "", 204

@app.route("/api/stats", methods=["GET"])
def get_stats():
all_tags = []
for note in notes.values():
all_tags.extend(note.get("tags", []))

tag_counts = {}
for tag in all_tags:
tag_counts[tag] = tag_counts.get(tag, 0) + 1

return jsonify({
"total_notes": len(notes),
"tags": tag_counts
})

if __name__ == "__main__":
# Add some sample data
sample_notes = [
{"title": "Learn Python", "content": "Start with the basics", "tags": ["python", "learning"]},
{"title": "Build a project", "content": "Apply what you learn", "tags": ["projects", "learning"]},
{"title": "Git commands", "content": "add, commit, push, pull", "tags": ["git", "tools"]},
]

for note_data in sample_notes:
note = {
"id": next_id,
"title": note_data["title"],
"content": note_data["content"],
"tags": note_data["tags"],
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat()
}
notes[next_id] = note
next_id += 1

print("API running at http://localhost:5000")
print("Try: curl http://localhost:5000/api/notes")
app.run(debug=True)


Test it:
bash
# Get all notes
curl http://localhost:5000/api/notes

# Create a note curl -X POST http://localhost:5000/api/notes \ -H "Content-Type: application/json" \ -d '{"title": "New note", "content": "Hello world", "tags": ["test"]}' # Update a note curl -X PUT http://localhost:5000/api/notes/1 \ -H "Content-Type: application/json" \ -d '{"title": "Updated title"}' # Delete a note curl -X DELETE http://localhost:5000/api/notes/1 # Filter by tag curl http://localhost:5000/api/notes?tag=learning # Get stats curl http://localhost:5000/api/stats

What you learn: How web APIs actually work under the hood, HTTP methods and status codes, request/response cycles, URL routing, query parameters, and JSON serialization. This is foundational knowledge for any web developer.

Extend it: Add authentication. Add pagination for the GET endpoint. Connect to a real database. Add search functionality.

Project 10: Basic Chatbot

Concepts: classes, pattern matching, state management, text processing

A rule-based chatbot that can hold basic conversations and answer questions.

python
import re
import random
from datetime import datetime

class Chatbot:
def __init__(self, name="PyBot"):
self.name = name
self.context = {}
self.conversation_history = []

# Pattern-response pairs self.patterns = [ # Greetings (r"\b(hi|hello|hey|greetings|howdy)\b", ["Hello! How can I help you today?", "Hey there! What's on your mind?", "Hi! Nice to chat with you."]), # How are you (r"how are you|how('s| is) it going|what('s| is) up", ["I'm doing great, thanks for asking! How about you?", "Running smoothly! What can I help you with?", "All good here! What would you like to talk about?"]), # Name questions (r"what('s| is) your name|who are you", [f"I'm {name}, a simple chatbot built with Python!", f"My name is {name}. I was created as a Python project."]), # User introduces themselves (r"my name is (\w+)|i('m| am) (\w+)", None), # Handled specially # Time (r"what time|current time|what('s| is) the time", None), # Handled specially # Date (r"what('s| is) the date|today('s| is) date|what day", None), # Handled specially # Python questions (r"what is python|tell me about python", ["Python is a high-level, interpreted programming language known for its readability and versatility. It's used in web development, data science, AI, automation, and much more.", "Python is one of the most popular programming languages. It was created by Guido van Rossum and emphasizes code readability."]), # Programming advice (r"how (do i|to|can i) learn (to )?(code|program)", ["Start with one language (Python is great for beginners), build small projects, and practice consistently. The key is writing code every day, even just for 30 minutes.", "Pick a language, follow a structured tutorial, then immediately start building projects. Reading alone won't teach you -- you have to write code."]), # Capabilities (r"what can you do|help|what do you know", [f"I can chat about various topics, tell you the time and date, remember your name, and answer basic questions about programming. I'm a simple rule-based bot -- try me!", "I'm a basic chatbot. I can tell you the time, answer simple questions, and have a conversation. Ask me anything!"]), # Thanks (r"\b(thanks|thank you|thx)\b", ["You're welcome!", "Happy to help!", "No problem at all!"]), # Goodbye (r"\b(bye|goodbye|see you|quit|exit)\b", ["Goodbye! Have a great day!", "See you later!", "Bye! It was nice chatting with you."]), # Jokes (r"tell me a joke|joke|make me laugh", ["Why do programmers prefer dark mode? Because light attracts bugs.", "A SQL query walks into a bar, walks up to two tables and asks: 'Can I join you?'", "There are only 10 types of people in the world: those who understand binary and those who don't.", "Why was the JavaScript developer sad? Because he didn't Node how to Express himself."]), # Feelings (r"i('m| am) (sad|upset|depressed|unhappy)", ["I'm sorry to hear that. Remember, it's okay to not be okay sometimes. Is there anything specific that's bothering you?", "That's tough. Sometimes taking a break and doing something you enjoy can help. Want to talk about what's going on?"]),

(r"i('m| am) (happy|great|good|excellent|wonderful)",
["That's wonderful to hear! What's making your day great?",
"Awesome! Glad you're doing well!"]),
]

def get_response(self, user_input):
text = user_input.lower().strip()
self.conversation_history.append({"role": "user", "text": user_input})

# Check for name introduction name_match = re.search(r"my name is (\w+)|i(?:'m| am) (\w+)", text) if name_match: name = name_match.group(1) or name_match.group(2) # Filter out common non-name words if name not in ("doing", "fine", "good", "great", "okay", "happy", "sad", "looking", "trying"): self.context["user_name"] = name.title() response = f"Nice to meet you, {self.context['user_name']}! How can I help you today?" self.conversation_history.append({"role": "bot", "text": response}) return response # Check for time question if re.search(r"what time|current time|what('s| is) the time", text): now = datetime.now().strftime("%I:%M %p") response = f"The current time is {now}." self.conversation_history.append({"role": "bot", "text": response}) return response # Check for date question if re.search(r"what('s| is) the date|today('s| is) date|what day", text): today = datetime.now().strftime("%A, %B %d, %Y") response = f"Today is {today}." self.conversation_history.append({"role": "bot", "text": response}) return response # Check patterns for pattern, responses in self.patterns: if responses is None: continue if re.search(pattern, text): response = random.choice(responses) # Personalize if we know the user's name if "user_name" in self.context and random.random() > 0.5: response = f"{self.context['user_name']}, " + response[0].lower() + response[1:]

self.conversation_history.append({"role": "bot", "text": response})
return response

# Default responses defaults = [ "Interesting! Tell me more about that.", "I'm not sure I understand. Could you rephrase that?", "That's a good question. I don't have a great answer for that one.", "Hmm, I'll have to think about that. What else is on your mind?", "I'm still learning! Could you try asking in a different way?" ]

response = random.choice(defaults)
self.conversation_history.append({"role": "bot", "text": response})
return response

def is_goodbye(self, text):
return bool(re.search(r"\b(bye|goodbye|quit|exit)\b", text.lower()))

def main():
bot = Chatbot("PyBot")

print(f"\n{bot.name}: Hello! I'm {bot.name}, a simple chatbot.")
print(f"{bot.name}: Type 'quit' to exit.\n")

while True:
user_input = input("You: ").strip()

if not user_input:
continue

response = bot.get_response(user_input)
print(f"{bot.name}: {response}\n")

if bot.is_goodbye(user_input):
break

if __name__ == "__main__":
main()
```

What you learn: Object-oriented programming in practice (a class managing state and behavior), regular expressions for pattern matching, state management (remembering the user's name across the conversation), and the architecture behind simple conversational systems. Extend it: Add the ability to learn new responses. Integrate with an actual language model API. Add conversation logging and analytics. Build a web interface with Flask.

What All These Projects Have in Common

Every project in this list follows the same fundamental pattern:

  1. Take input (user input, file, API)
  2. Process it (transform, filter, calculate, store)
  3. Produce output (display, save, send)
That's what every program does. A web app takes HTTP requests, processes data, and returns HTML. A machine learning model takes training data, adjusts weights, and produces predictions. A game takes player input, updates game state, and renders frames.

The specific technologies change. The pattern doesn't.

The Right Way to Use These Projects

Don't just read the code. Here's the approach that actually builds skill:

  1. Read the description of what the project does
  2. Try to build it yourself before looking at the code
  3. When stuck, look at just enough of the code to get unstuck
  4. After finishing, compare your solution to the one here
  5. Extend it with the suggestions at the end of each project
  6. Build something similar with a different twist
The projects here are starting points, not endpoints. The password generator could become a password manager. The web scraper could become a price tracker. The chatbot could become a customer service tool.

Your version doesn't have to be better. It just has to be yours.

Find more structured projects and challenges at CodeUp -- building things is the fastest way to stop feeling like a beginner and start feeling like a developer.

Ad 728x90