Innovate | Connect | Transform
ContactService statusUSD $

GICT knowledge base

10 Mini Python Projects with Tested Examples and Safety Checks

Published

Python

These ten small Python projects practice input handling, collections, loops, files and HTTP requests. Each example has a defined scope and a check you can repeat. Start in a new practice directory with synthetic data; none of these exercises requires a public server.

Reviewed and tested October 14, 2026: All ten examples were covered by 15 local test groups using Python 3.12.14. Weather responses and the countdown clock were mocked; no live weather API, real contact list or production file organizer was tested. These are learning programs, not finished applications.

Before you start

Run each example as its own .py file using your installed Python 3 command. Nine examples use only the standard library. The weather example additionally needs Requests, which can be installed in a project virtual environment with python -m pip install requests. Tests used Requests 2.34.2.

Interactive examples expect terminal input. End-of-file and keyboard interruption can stop them; a polished application would decide how to handle those exits. Save work separately and avoid putting secrets or personal information in practice files.

1. Calculator: operations and validation

Practice functions and branching with all four operations. The program rejects invalid operations, division by zero and non-finite values instead of silently doing nothing.

import math


def calculate(a, operation, b):
    if not math.isfinite(a) or not math.isfinite(b):
        raise ValueError("Use finite numbers.")
    if operation == "+":
        result = a + b
    elif operation == "-":
        result = a - b
    elif operation == "*":
        result = a * b
    elif operation == "/":
        if b == 0:
            raise ValueError("Cannot divide by zero.")
        result = a / b
    else:
        raise ValueError("Choose +, -, * or /.")
    if not math.isfinite(result):
        raise ValueError("Result is outside the supported range.")
    return result


try:
    first = float(input("First number: "))
    operation = input("Operation: ").strip()
    second = float(input("Second number: "))
    print("Result:", calculate(first, operation, second))
except ValueError as error:
    print("Invalid calculation:", error)

Try: 8 / 2, 1 / 0 and a non-numeric input. Floating-point arithmetic is a learning choice here, not a currency-calculation design. An extension is a repeat menu or decimal-based arithmetic with an explicit rounding policy.

2. To-do list: lists and numbered removal

Keep tasks in memory and remove them by their displayed number. Duplicate task text is allowed; invalid numbers must not remove a different item or crash the program.

tasks = []
while True:
    choice = input("Choose add/view/remove/exit: ").strip().lower()
    if choice == "exit":
        break
    if choice == "add":
        task = input("Task: ").strip()
        if task:
            tasks.append(task)
        else:
            print("Task cannot be empty.")
    elif choice == "view":
        if not tasks:
            print("No tasks.")
        for index, task in enumerate(tasks, 1):
            print(f"{index}. {task}")
    elif choice == "remove":
        try:
            number = int(input("Task number to remove: "))
            if not 1 <= number <= len(tasks):
                raise ValueError
        except ValueError:
            print("Choose a task number shown by view.")
        else:
            print("Removed:", tasks.pop(number - 1))
    else:
        print("Unknown command.")

Try: Add the same text twice, view the numbers, then remove one item. Try zero and an out-of-range number. Tasks disappear when the program exits; persistent storage is a separate extension that needs a file format and safe overwrite behavior.

3. Guessing game: loops and input recovery

The computer chooses an integer from 1 through 100. The loop handles invalid input and lets the player quit rather than forcing a correct guess.

import random

number = random.randint(1, 100)
while True:
    entry = input("Guess 1-100, or q to quit: ").strip()
    if entry.lower() == "q":
        break
    try:
        guess = int(entry)
    except ValueError:
        print("Enter a whole number.")
        continue
    if not 1 <= guess <= 100:
        print("Choose a number from 1 through 100.")
    elif guess < number:
        print("Too low!")
    elif guess > number:
        print("Too high!")
    else:
        print("You got it!")
        break

Try: Enter a word, zero, then a valid number. The tests fixed the random target to check low, high and correct guesses reproducibly. As an extension, count valid attempts or let the player choose a range.

4. Quiz: structured questions and scoring

A list of dictionaries keeps each question, its options and its answer together. Normalizing surrounding spaces and letter case avoids rejecting a correct answer solely for presentation.

questions = [
    {"question": "Which type is mutable?", "options": ["tuple", "list"], "answer": "list"},
    {"question": "What is 2 + 2?", "options": ["3", "4", "5"], "answer": "4"}
]
score = 0
for question in questions:
    print(question["question"])
    print("Options:", ", ".join(question["options"]))
    answer = input("Your answer: ").strip().casefold()
    if answer == question["answer"].casefold():
        score += 1
print(f"Score: {score}/{len(questions)}")

Try: Answer with LIST and 4, then deliberately answer both incorrectly. Extend the data with more questions before adding timers or a separate question file.

5. Password demonstration: security-sensitive randomness

Use the secrets module, not the simulation-oriented random module, for password generation. This creates a 20-character demonstration value; it does not guarantee that every character class appears or satisfy every site’s password policy.

import secrets
import string

length = 20
characters = string.ascii_letters + string.digits + string.punctuation
password = "".join(secrets.choice(characters) for _ in range(length))
print(password)

Try: Inspect the length and allowed character set without keeping the output. Use a password manager for actual credentials. Do not save plaintext passwords in a practice file, terminal recording or application log, and do not treat a homemade strength meter as a security assessment.

6. Text adventure: choices that actually retry

A small decision loop is enough to practice branching. Unlike an example that prints “try again” and then exits, this version asks again until the player chooses a listed option.

print("Two paths lead away from your campsite.")
while True:
    choice = input("Choose left/right/quit: ").strip().lower()
    if choice == "left":
        print("You find a bridge across the river.")
        break
    if choice == "right":
        print("You reach a quiet meadow.")
        break
    if choice == "quit":
        print("You stay at the campsite.")
        break
    print("Choose one of the listed paths.")

Try: Enter an unknown choice before selecting each route on separate runs. Later, put scenes in functions or add a small inventory without combining the entire story into one large conditional.

7. Contact book: dictionaries without silent replacement

Use fake contacts only. This in-memory example rejects an existing name rather than silently overwriting that person’s details. Names are case-sensitive keys; a real address book should use stable identifiers and distinguish display names from identity.

contacts = {}
while True:
    choice = input("Choose add/view/delete/exit: ").strip().lower()
    if choice == "exit":
        break
    if choice == "add":
        name = input("Unique contact name: ").strip()
        if not name or name in contacts:
            print("Use a non-empty name that is not already stored.")
            continue
        contacts[name] = {"phone": input("Phone: ").strip(),
                          "email": input("Email: ").strip()}
    elif choice == "view":
        for name, info in sorted(contacts.items()):
            print(name, info)
    elif choice == "delete":
        name = input("Name to delete: ").strip()
        if name in contacts:
            del contacts[name]
            print("Deleted:", name)
        else:
            print("Contact not found.")
    else:
        print("Unknown command.")

Try: Add a contact, attempt to add the same name again, then delete a nonexistent name. This does not validate telephone numbers or email ownership and does not save data after exit. File persistence and editing are separate exercises.

8. Countdown: elapsed time rather than loop counts

Use a monotonic clock to calculate the remaining duration. Counting loop iterations alone adds processing delays to the requested time. This remains an ordinary terminal timer, not a real-time scheduling guarantee.

import math
import time

try:
    seconds = int(input("Seconds, 0-86400: "))
    if not 0 <= seconds <= 86400:
        raise ValueError
except ValueError:
    print("Enter a whole number from 0 through 86400.")
else:
    deadline = time.monotonic() + seconds
    last_display = None
    while True:
        remaining = deadline - time.monotonic()
        if remaining <= 0:
            break
        displayed = math.ceil(remaining)
        if displayed != last_display:
            print("Time left:", displayed)
            last_display = displayed
        time.sleep(min(0.1, remaining))
    print("Time is up!")

Try: Zero, three seconds and an invalid negative number. The tests advanced a fake clock instead of waiting; they check the logic, not the precision of an operating system’s scheduler. An extension is a minutes-and-seconds display.

9. Weather client: coordinates, credentials and failures

The OpenWeather current-weather API documents latitude and longitude requests and deprecates its built-in city-name geocoder. This example therefore asks for coordinates. Check the provider’s current access terms and limits before obtaining a key.

Set OPENWEATHER_API_KEY privately in the environment used by the program. Do not put a real key in the source, repository, screenshots or a shared shell command. The provider requires it as a request parameter, so avoid logging request URLs.

import math
import os
import requests


def current_weather(latitude, longitude, api_key):
    if not api_key:
        raise ValueError("Set OPENWEATHER_API_KEY in your environment.")
    if not (math.isfinite(latitude) and math.isfinite(longitude)
            and -90 <= latitude <= 90 and -180 <= longitude <= 180):
        raise ValueError("Latitude or longitude is outside its valid range.")
    try:
        response = requests.get(
            "https://api.openweathermap.org/data/2.5/weather",
            params={"lat": latitude, "lon": longitude, "appid": api_key, "units": "metric"},
            timeout=10, allow_redirects=False
        )
    except requests.RequestException:
        raise RuntimeError("Weather request failed; check the connection and try later.") from None
    if response.status_code != 200:
        raise RuntimeError(f"Weather service returned HTTP {response.status_code}.")
    try:
        data = response.json()
        temperature = float(data["main"]["temp"])
        description = data["weather"][0]["description"]
        if not math.isfinite(temperature) or not isinstance(description, str):
            raise ValueError
    except (ValueError, KeyError, TypeError, IndexError):
        raise RuntimeError("Weather service returned an unexpected response.") from None
    return temperature, description


if __name__ == "__main__":
    try:
        latitude = float(input("Latitude: "))
        longitude = float(input("Longitude: "))
        temperature, description = current_weather(latitude, longitude, os.getenv("OPENWEATHER_API_KEY"))
        print(f"Temperature: {temperature} C; weather: {description}")
    except (ValueError, RuntimeError) as error:
        print(error)

Try: A missing key and invalid coordinates before making a real request. Mocked tests covered success, timeouts, HTTP errors, malformed JSON and unexpected fields. They did not validate an account’s subscription or make a live weather call. The Requests documentation explains timeout and exception behavior; the timeout here is not a guaranteed total runtime limit.

A 401, 429 or server error is not necessarily a nonexistent location. This version reports the HTTP status without echoing the key-bearing URL and refuses unexpected redirects.

10. File organizer: plan before moving

Replace the placeholder with an existing directory containing disposable file copies. This dry run prints intended image moves, handles uppercase extensions and skips existing destinations. It does not create folders, move files or delete anything.

from pathlib import Path

folder = Path("your/folder/path")
if not folder.is_dir():
    raise ValueError("Choose an existing test directory")

for source in sorted(folder.iterdir()):
    if source.is_symlink() or not source.is_file():
        continue
    if source.suffix.lower() not in {".jpg", ".png"}:
        continue
    destination = folder / "Images" / source.name
    if destination.exists():
        print("SKIP, destination exists:", destination.name)
    else:
        print("WOULD MOVE:", source.name, "-> Images/" + source.name)

Try: A .JPG, a .png, an unrelated text file and a destination that already exists. The test verified that the files and existing destination content remained unchanged. An actual apply mode needs backups, collision handling and protection against path changes between checking and moving; do not simply add an unchecked move call.

Build on the examples with tests

Choose one project and define its expected behavior before expanding it. Keep tests for normal input, invalid input and an empty state. For operations involving files, secrets or paid services, use disposable fixtures and mocked responses first. A working happy path is the beginning of a program, not proof it is safe for production use.

Related guides

Account services are unavailable until the production backend is configured.