15 Python Mini Projects for Beginners (With Ideas and Starter Code)

Reading syntax tutorials teaches you what a for loop does. Building a small project teaches you when to reach for one. This guide gives you 15 beginner-friendly Python mini projects, each with a clear goal, the concepts it trains, and starter code you can run immediately. The projects progress from basic input and output to file handling, APIs, and simple object-oriented design.

Quick Takeaways

  • 15 projects are ordered by difficulty, from 10-line scripts to small multi-function programs.
  • Each project trains a specific skill: string handling, loops, dictionaries, file I/O, classes, HTTP requests, or modules.
  • All starter code uses the Python 3.10+ standard library, except two that use requests.
  • Extend every project with the “Level Up” idea to turn a tutorial exercise into a portfolio piece.
# Project Core Concept Difficulty
1 Number Guessing Game Loops, random Easy
2 Tip Calculator Input, float math Easy
3 Password Generator secrets, strings Easy
4 Rock Paper Scissors Conditionals, dicts Easy
5 Unit Converter Functions, dicts Easy
6 To-Do List (CLI) Lists, loops Easy
7 Word Counter collections.Counter Medium
8 Dice Roller Simulator random, validation Medium
9 Contact Book Dicts, JSON file I/O Medium
10 Quiz App Data structures Medium
11 Expense Tracker CSV, aggregation Medium
12 Hangman State management Medium
13 Weather CLI requests, JSON Medium
14 Bank Account Class OOP Advanced beginner
15 File Organizer pathlib, shutil Advanced beginner

How to Use These Python Beginner Projects

Follow this loop for each project:

  1. Read the goal and write the steps in plain English first.
  2. Type the starter code yourself. Do not copy and paste.
  3. Break it on purpose. Enter letters where numbers are expected.
  4. Extend it using the Level Up idea.

Set up a virtual environment once so projects stay isolated:

python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate

Run any script with python filename.py.

Beginner Projects (1-6)

1. Number Guessing Game

Concepts: while loops, random.randint(), int() conversion, exception handling.

The computer picks a number. The player guesses until correct.

import random

def play(low: int = 1, high: int = 100) -> None:
    secret = random.randint(low, high)
    attempts = 0

    while True:
        raw = input(f"Guess a number between {low} and {high}: ")
        try:
            guess = int(raw)
        except ValueError:
            print("Enter a whole number.")
            continue

        attempts += 1
        if guess < secret:
            print("Too low.")
        elif guess > secret:
            print("Too high.")
        else:
            print(f"Correct! You needed {attempts} attempts.")
            break

if __name__ == "__main__":
    play()

The try/except block catches ValueError so typing abc does not crash the program. The if __name__ == "__main__" guard lets you import play() elsewhere without running the game.

Level Up: Limit attempts to 7 and add a difficulty menu.

2. Tip Calculator

Concepts: float(), formatted strings, basic arithmetic.

def calculate_tip(bill: float, tip_percent: float, people: int = 1) -> dict:
    tip = bill * tip_percent / 100
    total = bill + tip
    return {"tip": round(tip, 2), "total": round(total, 2),
            "per_person": round(total / people, 2)}

bill = float(input("Bill amount: "))
percent = float(input("Tip percent: "))
people = int(input("Split between how many people? "))

result = calculate_tip(bill, percent, people)
print(f"Tip: ${result['tip']:.2f}")
print(f"Total: ${result['total']:.2f}")
print(f"Each pays: ${result['per_person']:.2f}")

Entering 50, 20, 2 prints a tip of $10.00, total of $60.00, and $30.00 each. The :.2f format spec forces two decimal places.

Level Up: Reject negative values and a zero head count.

3. Password Generator

Concepts: the secrets module, string constants, str.join().

Use secrets, not random, for anything security-related. random is predictable. secrets draws from the operating system’s cryptographic source.

import secrets
import string

def generate_password(length: int = 16, symbols: bool = True) -> str:
    alphabet = string.ascii_letters + string.digits
    if symbols:
        alphabet += string.punctuation
    return "".join(secrets.choice(alphabet) for _ in range(length))

print(generate_password(20))

The generator expression builds one character per iteration, then "".join() concatenates them. Output differs on every run.

Level Up: Guarantee at least one digit, one uppercase letter, and one symbol.

4. Rock Paper Scissors

Concepts: dictionaries as lookup tables, conditionals.

import random

BEATS = {"rock": "scissors", "paper": "rock", "scissors": "paper"}

def judge(player: str, computer: str) -> str:
    if player == computer:
        return "Draw"
    return "You win" if BEATS[player] == computer else "You lose"

player = input("rock, paper, or scissors? ").strip().lower()
if player not in BEATS:
    print("Invalid choice.")
else:
    computer = random.choice(list(BEATS))
    print(f"Computer chose {computer}. {judge(player, computer)}.")

The BEATS dictionary replaces a chain of nine if statements. Each key maps to the choice it defeats.

Level Up: Add a best-of-five scoreboard, then add “lizard” and “spock”.

5. Unit Converter

Concepts: functions, dictionary-based dispatch.

CONVERSIONS = {
    ("km", "miles"): lambda x: x * 0.621371,
    ("miles", "km"): lambda x: x / 0.621371,
    ("kg", "lbs"): lambda x: x * 2.20462,
    ("lbs", "kg"): lambda x: x / 2.20462,
    ("c", "f"): lambda x: x * 9 / 5 + 32,
    ("f", "c"): lambda x: (x - 32) * 5 / 9,
}

def convert(value: float, src: str, dst: str) -> float:
    try:
        return CONVERSIONS[(src, dst)](value)
    except KeyError:
        raise ValueError(f"Unsupported conversion: {src} -> {dst}")

print(round(convert(100, "c", "f"), 2))   # 212.0
print(round(convert(10, "km", "miles"), 2))  # 6.21

A tuple key (src, dst) selects the right function. Adding a new conversion means adding one line, not another elif.

Level Up: Build a menu loop and support length, weight, and volume categories.

6. Command-Line To-Do List

Concepts: lists, enumerate(), menu loops.

def show(tasks: list[str]) -> None:
    if not tasks:
        print("No tasks.")
    for i, task in enumerate(tasks, start=1):
        print(f"{i}. {task}")

def main() -> None:
    tasks: list[str] = []
    while True:
        cmd = input("[a]dd, [d]elete, [l]ist, [q]uit: ").strip().lower()
        if cmd == "a":
            tasks.append(input("Task: ").strip())
        elif cmd == "d":
            show(tasks)
            idx = int(input("Number to delete: ")) - 1
            if 0 <= idx < len(tasks):
                tasks.pop(idx)
        elif cmd == "l":
            show(tasks)
        elif cmd == "q":
            break

main()

The bounds check 0 <= idx < len(tasks) prevents an IndexError. Tasks vanish when the program exits.

Level Up: Save tasks to a file (see project 9) and add a “done” flag.

Intermediate Beginner Projects (7-13)

7. Word Counter

Concepts: str.split(), collections.Counter, re.

import re
from collections import Counter

def top_words(text: str, n: int = 5) -> list[tuple[str, int]]:
    words = re.findall(r"[a-z']+", text.lower())
    return Counter(words).most_common(n)

sample = "the cat and the hat and the bat"
for word, count in top_words(sample, 3):
    print(f"{word}: {count}")

Output:

the: 3
and: 2
cat: 1

Counter.most_common() runs in roughly O(n log k) time. The regex strips punctuation so "hat," and "hat" count as one word.

Level Up: Read a .txt file and ignore stop words like “the” and “and”.

8. Dice Roller Simulator

Concepts: string parsing, input validation, random.

Parse notation like 2d6 (two six-sided dice).

import random
import re

def roll(notation: str) -> tuple[list[int], int]:
    match = re.fullmatch(r"(\d+)d(\d+)", notation.strip().lower())
    if not match:
        raise ValueError("Use the format NdM, e.g. 2d6")
    count, sides = map(int, match.groups())
    rolls = [random.randint(1, sides) for _ in range(count)]
    return rolls, sum(rolls)

rolls, total = roll("3d6")
print(f"Rolls: {rolls}, total: {total}")

re.fullmatch rejects strings with trailing junk, which re.match would accept.

Level Up: Support modifiers like 2d6+3.

9. Contact Book (JSON Storage)

Concepts: dictionaries, JSON serialization, file I/O, pathlib.

import json
from pathlib import Path

DB = Path("contacts.json")

def load() -> dict[str, str]:
    return json.loads(DB.read_text()) if DB.exists() else {}

def save(contacts: dict[str, str]) -> None:
    DB.write_text(json.dumps(contacts, indent=2))

contacts = load()
contacts["Ada Lovelace"] = "555-0100"
save(contacts)
print(load())

The script writes a human-readable contacts.json and reloads it on the next run. Data now persists between sessions.

Level Up: Add search, edit, and delete commands.

10. Quiz App

Concepts: lists of dictionaries, scoring logic.

QUESTIONS = [
    {"q": "Which keyword defines a function?", "a": "def"},
    {"q": "What does len([1, 2, 3]) return?", "a": "3"},
    {"q": "Is a tuple mutable? (yes/no)", "a": "no"},
]

def run_quiz(questions: list[dict]) -> int:
    score = 0
    for item in questions:
        answer = input(item["q"] + " ").strip().lower()
        if answer == item["a"]:
            score += 1
            print("Correct.")
        else:
            print(f"Wrong. Answer: {item['a']}")
    return score

score = run_quiz(QUESTIONS)
print(f"Score: {score}/{len(QUESTIONS)}")

Separating data (QUESTIONS) from logic (run_quiz) lets you swap question sets without touching the engine.

Level Up: Load questions from a JSON file and shuffle them with random.shuffle().

11. Expense Tracker

Concepts: CSV handling, datetime, aggregation with defaultdict.

import csv
from collections import defaultdict
from datetime import date
from pathlib import Path

FILE = Path("expenses.csv")

def add_expense(category: str, amount: float) -> None:
    new_file = not FILE.exists()
    with FILE.open("a", newline="") as f:
        writer = csv.writer(f)
        if new_file:
            writer.writerow(["date", "category", "amount"])
        writer.writerow([date.today().isoformat(), category, amount])

def totals_by_category() -> dict[str, float]:
    totals: dict[str, float] = defaultdict(float)
    with FILE.open() as f:
        for row in csv.DictReader(f):
            totals[row["category"]] += float(row["amount"])
    return dict(totals)

add_expense("food", 12.50)
add_expense("transport", 3.20)
add_expense("food", 8.00)
print(totals_by_category())

Output: {'food': 20.5, 'transport': 3.2}. Opening the file in append mode ("a") preserves earlier rows. defaultdict(float) removes the need to check whether a key exists.

Level Up: Filter totals by month and chart them with matplotlib.

12. Hangman

Concepts: sets, state tracking, string building.

import random

WORDS = ["python", "variable", "function", "iterator", "decorator"]

def hangman(max_wrong: int = 6) -> None:
    word = random.choice(WORDS)
    guessed: set[str] = set()
    wrong = 0

    while wrong < max_wrong:
        display = " ".join(c if c in guessed else "_" for c in word)
        print(display)
        if "_" not in display:
            print("You won!")
            return

        letter = input("Letter: ").strip().lower()
        if len(letter) != 1 or not letter.isalpha() or letter in guessed:
            print("Invalid or repeated.")
            continue

        guessed.add(letter)
        if letter not in word:
            wrong += 1
            print(f"Wrong. {max_wrong - wrong} left.")

    print(f"You lost. The word was '{word}'.")

hangman()

A set gives O(1) membership checks and rejects duplicate guesses automatically.

Level Up: Add ASCII art for each wrong guess.

13. Weather CLI (API Request)

Concepts: HTTP requests, JSON parsing, error handling. Requires pip install requests.

This example uses the free Open-Meteo API, which needs no key.

import requests

def current_temp(lat: float, lon: float) -> float:
    url = "https://api.open-meteo.com/v1/forecast"
    params = {"latitude": lat, "longitude": lon, "current_weather": True}
    response = requests.get(url, params=params, timeout=10)
    response.raise_for_status()
    return response.json()["current_weather"]["temperature"]

try:
    print(f"Temperature: {current_temp(40.71, -74.01)} °C")
except requests.RequestException as exc:
    print(f"Request failed: {exc}")

Always set timeout. Without it, requests can hang forever. raise_for_status() converts HTTP 4xx and 5xx responses into exceptions.

Level Up: Accept a city name and geocode it with Open-Meteo’s geocoding endpoint.

Advanced Beginner Projects (14-15)

14. Bank Account Class

Concepts: classes, encapsulation, custom exceptions, __repr__.

class InsufficientFunds(Exception):
    pass

class BankAccount:
    def __init__(self, owner: str, balance: float = 0.0) -> None:
        self.owner = owner
        self._balance = balance

    @property
    def balance(self) -> float:
        return self._balance

    def deposit(self, amount: float) -> None:
        if amount <= 0:
            raise ValueError("Deposit must be positive.")
        self._balance += amount

    def withdraw(self, amount: float) -> None:
        if amount > self._balance:
            raise InsufficientFunds(f"Balance {self._balance}, requested {amount}")
        self._balance -= amount

    def __repr__(self) -> str:
        return f"BankAccount({self.owner!r}, balance={self._balance:.2f})"

acct = BankAccount("Grace", 100)
acct.deposit(50)
acct.withdraw(30)
print(acct)  # BankAccount('Grace', balance=120.00)

The leading underscore on _balance signals “internal.” The @property exposes it read-only, so outside code cannot set the balance directly.

Level Up: Add a transaction history list and a SavingsAccount subclass with interest.

15. File Organizer

Concepts: pathlib, shutil, dictionaries, safe file operations.

import shutil
from pathlib import Path

CATEGORIES = {
    "Images": {".jpg", ".jpeg", ".png", ".gif"},
    "Documents": {".pdf", ".docx", ".txt"},
    "Archives": {".zip", ".tar", ".gz"},
}

def organize(folder: str, dry_run: bool = True) -> None:
    root = Path(folder)
    for item in root.iterdir():
        if not item.is_file():
            continue
        for category, extensions in CATEGORIES.items():
            if item.suffix.lower() in extensions:
                target = root / category
                print(f"{item.name} -> {category}/")
                if not dry_run:
                    target.mkdir(exist_ok=True)
                    shutil.move(str(item), target / item.name)
                break

organize("./Downloads", dry_run=True)

The dry_run flag previews every move before touching a file. Always build this safeguard into scripts that modify the filesystem.

Level Up: Handle filename collisions and log every move to a file.

Skills Matrix: What Each Project Teaches

Skill Projects
Loops and conditionals 1, 4, 6, 12
Dictionaries 4, 5, 9, 10
Functions and type hints 2, 5, 8
File I/O (JSON, CSV) 9, 11
Standard library modules 3, 7, 11, 15
External APIs 13
Object-oriented design 14
Error handling 1, 8, 13, 14

Bad Code vs. Good Code: Refactoring Beginner Scripts

Most first projects share the same weaknesses. Here is a before-and-after on the tip calculator.

Anti-pattern

# Bad: no validation, magic numbers, mixed concerns, crashes on bad input
b = float(input("bill"))
t = float(input("tip"))
print(b + b * t / 100)

Problems: cryptic names, no error handling, no reusable function, no formatting.

Refactored

def parse_positive(prompt: str) -> float:
    """Prompt until the user enters a positive number."""
    while True:
        try:
            value = float(input(prompt))
            if value > 0:
                return value
            print("Value must be greater than zero.")
        except ValueError:
            print("Enter a valid number.")

def total_with_tip(bill: float, tip_percent: float) -> float:
    return round(bill * (1 + tip_percent / 100), 2)

if __name__ == "__main__":
    bill = parse_positive("Bill amount: ")
    tip = parse_positive("Tip percent: ")
    print(f"Total: ${total_with_tip(bill, tip):.2f}")
Aspect Bad Code Good Code
Naming b, t bill, tip_percent
Input safety Crashes on text Retries until valid
Reusability None Pure function, testable
Importable No Yes, via __main__ guard

Common Mistakes to Avoid

  • Mutable default arguments. def add(item, bucket=[]) shares one list across all calls. Use bucket=None and create the list inside.
  • Using random for passwords. Use secrets.
  • Opening files without a context manager. Always use with open(...).
  • Catching bare except:. It hides bugs. Catch the specific exception.
  • Skipping the __main__ guard. Your script then runs on import.

What to Do After These Projects

  1. Put every project in a Git repository with a short README.
  2. Add pytest tests for the pure functions (judge(), convert(), top_words()).
  3. Combine projects: wire the expense tracker to a Flask or FastAPI endpoint.
  4. Learn virtual environments and requirements.txt to share your work.
  5. Read your old code a week later and refactor it.

FAQ

What is the best first Python project for a complete beginner?

The number guessing game. It uses only variables, a loop, a conditional, and one import. You can finish it in under 30 minutes and it teaches input validation immediately.

How many Python projects do I need before applying for jobs?

Three to five polished projects beat fifteen unfinished ones. Pick projects that show different skills, such as an API client, a file-based data tool, and a class-based design. Include tests and a README for each.

Can I build these projects without installing any libraries?

Yes. Fourteen of the fifteen use only the standard library. Only the Weather CLI requires the third-party requests package, installed with pip install requests.

How long does each Python mini project take?

Projects 1-6 take 20-60 minutes. Projects 7-13 take one to two hours. Projects 14-15 take two to three hours including the Level Up extensions.

Hot this week

Robotics Predictions for 2027

Seven data-driven robotics predictions for 2027: VLA policies, humanoid actuators, ROS 2 real-time, safety filters, and code. Read the engineering roadmap.

Robots and Jobs: What the Evidence Says in 2026

Robots and jobs in 2026: IFR installation data, wage and employment estimates, and a payback model engineers can run. Read the evidence.

Robotics Careers in 2026: Roles, Skills, and Salaries

Explore robotics careers in 2026: top roles, must-have ROS 2 and controls skills, and U.S. salary ranges. Build your robotics career roadmap today.

Robot Sensors Explained: How Robots Perceive the World

Learn how LiDAR, IMU, encoders, and depth cameras work, with fusion math and ROS2 code. Build a reliable perception stack today.

20 Beginner Robot Projects Ranked by Difficulty

Build 20 beginner robot projects ranked from easiest to hardest, with wiring, code, and ROS2 steps. Pick your first build and start today.

Topics

Robotics Predictions for 2027

Seven data-driven robotics predictions for 2027: VLA policies, humanoid actuators, ROS 2 real-time, safety filters, and code. Read the engineering roadmap.

Robots and Jobs: What the Evidence Says in 2026

Robots and jobs in 2026: IFR installation data, wage and employment estimates, and a payback model engineers can run. Read the evidence.

Robotics Careers in 2026: Roles, Skills, and Salaries

Explore robotics careers in 2026: top roles, must-have ROS 2 and controls skills, and U.S. salary ranges. Build your robotics career roadmap today.

Robot Sensors Explained: How Robots Perceive the World

Learn how LiDAR, IMU, encoders, and depth cameras work, with fusion math and ROS2 code. Build a reliable perception stack today.

20 Beginner Robot Projects Ranked by Difficulty

Build 20 beginner robot projects ranked from easiest to hardest, with wiring, code, and ROS2 steps. Pick your first build and start today.

Robot Computers in 2026: Raspberry Pi 5, Jetson, and Industrial PCs Compared

Raspberry Pi 5 vs NVIDIA Jetson vs industrial PCs for ROS 2 robots. See specs, latency math and a decision matrix, then pick your compute.

AI in Robotics: Programmed Behavior vs Machine Learning (and When You Need Each)

Programmed control or machine learning? Compare PID, FSMs, and RL with ROS2 code, benchmarks, and a decision framework. Pick the right one.

What Is ROS 2? A Beginner’s Guide to the Robot Operating System in 2026

Learn what ROS 2 is, how nodes, topics, and DDS work, and build your first publisher and subscriber. Start your ROS 2 project today.

Related Articles

Popular Categories