11 Python Projects for Beginners (With Starter Code)
Eleven small Python projects that teach real skills, from a number guessing game to a weather app, each with the modules it uses and starter code.
By PC MomentUpdated 8 min read

The fastest way to get better at Python is to build small programs that do something you can see. The eleven Python projects for beginners below start with a guessing game and end with programs that save data, organize files and talk to a web API.
Each project lists what it teaches, the modules it uses and a short starter sketch. Every sketch uses only the standard library and runs on Python 3.12 or later. Type it in, run it, then use the “take it further” ideas to make it your own.
If you haven’t set up Python yet, start with our guide on how to learn Python for free, which covers installing it and the free courses worth your time.
How to use these projects
Save each sketch as a .py file and run it from a terminal:
python guess.py
On Windows, py guess.py also works if you installed Python with the Python install manager. Work through the list in order: each project reuses ideas from the ones before it.
Tip: Don’t copy and paste. Typing the code yourself is slower, but you’ll notice the details (colons, indentation, quotes) that cause most beginner errors.
1. Number guessing game
What it teaches: input(), while loops, if/elif/else, converting text to numbers and catching bad input.
Modules: random
import random
secret = random.randint(1, 100)
tries = 0
while True:
guess_text = input("Guess a number from 1 to 100: ")
try:
guess = int(guess_text)
except ValueError:
print("That's not a whole number. Try again.")
continue
tries += 1
if guess < secret:
print("Too low.")
elif guess > secret:
print("Too high.")
else:
print(f"Got it in {tries} tries!")
break
random.randint(1, 100) returns a number from 1 to 100, including both ends. If you type something like abc, int() raises a ValueError, the except block prints a message and continue asks again.
Take it further: limit the player to seven guesses, or ask “Play again?” at the end.
2. Rock, paper, scissors
What it teaches: dictionaries, cleaning up user input with .strip().lower() and comparing values.
Modules: random
import random
BEATS = {"rock": "scissors", "paper": "rock", "scissors": "paper"}
player = input("rock, paper or scissors? ").strip().lower()
if player not in BEATS:
print("Please type rock, paper or scissors.")
else:
computer = random.choice(list(BEATS))
print(f"Computer picked {computer}.")
if player == computer:
print("It's a tie.")
elif BEATS[player] == computer:
print("You win!")
else:
print("You lose.")
The BEATS dictionary stores which move each move beats, so one lookup decides the winner instead of a long chain of if statements. random.choice() picks one item from a non-empty sequence.
Take it further: play best of five and keep score.
3. Password generator
What it teaches: functions with default arguments, string constants and generating secure random values.
Modules: secrets, string
import secrets
import string
def make_password(length=16):
alphabet = string.ascii_letters + string.digits + string.punctuation
return "".join(secrets.choice(alphabet) for _ in range(length))
print(make_password())
print(make_password(24))
This prints a random 16-character password and a 24-character one. Notice it uses secrets, not random. The Python docs warn that the random module should not be used for security purposes and point to secrets instead.
Take it further: keep generating until the password has at least one digit and one symbol. The secrets docs include a similar recipe that loops until the password has enough lowercase, uppercase and digit characters.
4. Quiz game
What it teaches: lists of dictionaries, for loops and keeping a running score.
Modules: none
QUESTIONS = [
{"question": "What keyword defines a function?", "answer": "def"},
{"question": "What does len('python') return?", "answer": "6"},
{"question": "Is [1, 2, 3] a list or a tuple?", "answer": "list"},
]
score = 0
for item in QUESTIONS:
reply = input(item["question"] + " ").strip().lower()
if reply == item["answer"]:
print("Correct!")
score += 1
else:
print(f"Not quite. The answer is {item['answer']}.")
print(f"You scored {score} out of {len(QUESTIONS)}.")
Storing questions as data (instead of writing a separate if for each one) means you can add a question without touching the logic.
Take it further: move the questions into a questions.json file and load them with json.load(), then shuffle them with random.shuffle().
5. Countdown timer
What it teaches: while loops that count down, divmod() and formatting numbers with leading zeros.
Modules: time
import time
def countdown(seconds):
while seconds > 0:
minutes, secs = divmod(seconds, 60)
print(f"{minutes:02d}:{secs:02d}", end="\r", flush=True)
time.sleep(1)
seconds -= 1
print("Time's up!")
countdown(int(input("Seconds: ")))
divmod(90, 60) gives (1, 30), which prints as 01:30. end="\r" moves the cursor back to the start of the line so each new time overwrites the last one in a terminal, and flush=True makes the text appear right away instead of waiting for a newline.
Take it further: build a Pomodoro timer that alternates 25 minutes of work with 5-minute breaks.
6. To-do list app
What it teaches: saving data between runs, reading and writing JSON, and a simple command loop.
Modules: json, pathlib
import json
from pathlib import Path
TODO_FILE = Path("todo.json")
def load_tasks():
if TODO_FILE.exists():
return json.loads(TODO_FILE.read_text(encoding="utf-8"))
return []
def save_tasks(tasks):
TODO_FILE.write_text(json.dumps(tasks, indent=2), encoding="utf-8")
tasks = load_tasks()
while True:
command = input("add, list, done or quit? ").strip().lower()
if command == "add":
tasks.append(input("Task: "))
save_tasks(tasks)
elif command == "list":
for number, task in enumerate(tasks, start=1):
print(f"{number}. {task}")
elif command == "done":
number = int(input("Task number: "))
print("Removed:", tasks.pop(number - 1))
save_tasks(tasks)
elif command == "quit":
break
Your tasks are stored in todo.json next to the script, so they’re still there the next time you run it. indent=2 makes the file readable if you open it in a text editor.
Take it further: typing x or 99 at “Task number” crashes the program. Catch ValueError and IndexError and print a friendly message instead. Our guide to raising exceptions in Python explains how try/except works.
7. Expense tracker
What it teaches: CSV files, dates and adding up values by category.
Modules: csv, datetime, pathlib
import csv
from datetime import date
from pathlib import Path
LOG = Path("expenses.csv")
def add_expense(amount, category):
is_new = not LOG.exists()
with LOG.open("a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
if is_new:
writer.writerow(["date", "amount", "category"])
writer.writerow([date.today().isoformat(), f"{amount:.2f}", category])
def totals_by_category():
totals = {}
with LOG.open(newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
category = row["category"]
totals[category] = totals.get(category, 0) + float(row["amount"])
return totals
add_expense(12.50, "food")
add_expense(40, "gas")
add_expense(7.25, "food")
print(totals_by_category())
On the first run this prints {'food': 19.75, 'gas': 40.0}. The file is opened with newline="" because the csv docs say to: without it, quoted fields with line breaks aren’t read correctly and Windows can get an extra \r on each row. DictReader uses the first row as field names, so you can write row["amount"] instead of row[1]. Because the file opens in append mode, running the script again adds three more rows.
Take it further: ask for the amount and category with input(), and open the CSV in Excel or Google Sheets to check it.
8. File organizer
What it teaches: working with folders and file extensions, and making a script safe to run twice.
Modules: pathlib
Important: This script moves files. Point it at a test folder with a few copied files first, never at a folder whose only copy of something matters.
from pathlib import Path
FOLDERS = {
".jpg": "Images", ".png": "Images",
".pdf": "Documents", ".docx": "Documents", ".txt": "Documents",
".zip": "Archives",
}
def organize(folder):
for item in list(Path(folder).iterdir()):
if not item.is_file():
continue
target_dir = item.parent / FOLDERS.get(item.suffix.lower(), "Other")
target_dir.mkdir(exist_ok=True)
target = target_dir / item.name
if target.exists():
print(f"Skipped {item.name}: already in {target_dir.name}")
continue
item.rename(target)
print(f"Moved {item.name} to {target_dir.name}")
organize(Path.home() / "Desktop" / "organizer-test")
item.suffix is the file extension (.pdf), and .lower() makes .JPG and .jpg land in the same folder. mkdir(exist_ok=True) creates the folder only if it doesn’t exist yet. The script skips any file that would overwrite another, and it wraps iterdir() in list() so it isn’t reading the folder while it changes it.
Take it further: add a “dry run” mode that only prints what it would move.
9. Weather app
What it teaches: calling a web API, building a URL with query parameters and reading JSON.
Modules: urllib.request, urllib.parse, json
This project uses Open-Meteo, a weather API that doesn’t need an API key for non-commercial use.
import json
import urllib.parse
import urllib.request
def current_weather(lat, lon):
params = urllib.parse.urlencode({
"latitude": lat,
"longitude": lon,
"current": "temperature_2m,wind_speed_10m",
"temperature_unit": "fahrenheit",
"wind_speed_unit": "mph",
})
url = f"https://api.open-meteo.com/v1/forecast?{params}"
with urllib.request.urlopen(url, timeout=10) as response:
data = json.load(response)
return data["current"]
now = current_weather(40.71, -74.01)
print(f"Temperature: {now['temperature_2m']} F, wind: {now['wind_speed_10m']} mph")
The coordinates are for New York City. The API returns Celsius and km/h by default, so the script asks for Fahrenheit and mph. The response’s current object holds one value for each variable you requested.
Take it further: let the user pick from a few saved cities, or add relative_humidity_2m to the current list.
10. Link collector (a first web scraper)
What it teaches: downloading a web page, decoding bytes to text and subclassing a class to react to HTML tags.
Modules: urllib.request, html.parser
from html.parser import HTMLParser
from urllib.request import Request, urlopen
class LinkCollector(HTMLParser):
def __init__(self):
super().__init__()
self.links = []
def handle_starttag(self, tag, attrs):
if tag == "a":
href = dict(attrs).get("href")
if href:
self.links.append(href)
url = "https://docs.python.org/3/"
request = Request(url, headers={"User-Agent": "beginner-link-collector/0.1"})
with urlopen(request, timeout=10) as response:
html = response.read().decode("utf-8", errors="replace")
parser = LinkCollector()
parser.feed(html)
print(f"Found {len(parser.links)} links. First five:")
for link in parser.links[:5]:
print(link)
urlopen() returns bytes, so the script decodes them to a string first. HTMLParser calls handle_starttag() for every opening tag, passing the attributes as a list of (name, value) pairs, which dict() turns into an easy lookup.
Note: Only scrape pages you’re allowed to. Check the site’s terms and its
robots.txtfile, and don’t send requests in a tight loop.
Take it further: once you’re comfortable, try the third-party Requests package, which the urllib.request docs recommend for a higher-level HTTP interface.
11. Word counter
What it teaches: regular expressions, counting with Counter and reading text files.
Modules: re, collections
import re
from collections import Counter
text = """Python is easy to read. Python is also easy to write,
and reading Python code is a good way to learn."""
words = re.findall(r"[a-z']+", text.lower())
counts = Counter(words)
print(f"{len(words)} words, {len(counts)} unique")
for word, count in counts.most_common(3):
print(word, count)
Output:
21 words, 14 unique
python 3
is 3
to 3
most_common(3) returns the three most frequent words, and words with the same count keep the order they first appeared in.
Take it further: read a real file with Path("essay.txt").read_text(encoding="utf-8") and skip common words like “the” and “and”.
What to build next
Once these feel easy, combine them: a to-do app that stores due dates and warns you about overdue tasks, or a weather script that logs readings to a CSV every day. Put your projects in version control as you go. If you make a mistake in a commit, our guide on how to undo the last commit in git shows how to back it out, and fixing “fatal: not a git repository” helps when git can’t find your project.
For more beginner guides and error fixes, browse the Programming section.
Frequently asked questions
What is a good first Python project?
A number guessing game. It fits in about 20 lines and covers input, loops, if/elif/else and handling bad input with try/except, which you will use in almost every later project.
Do I need to install any packages for these projects?
No. Every starter sketch here uses only Python's standard library, so a normal Python 3.12 or later install is enough. The weather app needs an internet connection.
Should I use the random module to generate passwords?
No. The Python docs say the random module should not be used for security purposes. Use the secrets module, which is designed for things like passwords and tokens.
How long does each project take?
The starter versions are short enough to type and run in one sitting. Most of the learning happens when you add the extension ideas, such as saving data to a file or handling bad input.
Related guides

Programming
How to Learn Python for Free: A Step-by-Step Path
A free path from zero to writing real Python: install it, pick one free course, use the official docs and build small projects.
7 min read

Programming
Python Raise Exception: How to Use raise, Custom Exceptions and raise from
How to raise exceptions in Python: built-in errors like ValueError, custom exception classes, raise from, re-raising and when to use assert instead.
7 min read

Programming
Fatal: Not a Git Repository: What It Means and How to Fix It
Git says "fatal: not a git repository" when it can't find a .git folder. Here's how to find the right folder, start a repository, and fix GIT_DIR, submodule and ownership problems.
6 min read

Programming
Java printf: Format Specifiers, Width, Precision and Flags
How System.out.printf and String.format work in Java: %d, %s, %f, %n and %x, width and precision, flags, locales and the mistakes that throw exceptions.
7 min read