Skip to content
PC Moment
Programming

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

PC Moment title card for Python Projects for Beginners

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.

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.txt file, 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