GICT knowledge base
10 Mini Python Projects with Tested Examples and Safety Checks
Published
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
How to Deploy a FastAPI App with Gunicorn and Nginx
Deploy FastAPI behind Nginx with Gunicorn and systemd. Separate application packages, check HTTPS and verify worker replacement and application health.
PythonLearn How to Modify Python Dictionary Values Easily
Learn how to modify Python dictionary values effectively with various methods, including direct assignment, update(), and comprehensions, along with common mistakes to...
PythonPython Set Copy Tutorial: Avoid Common Mistakes and Data Errors
Learn how to copy sets in Python safely and efficiently while avoiding common mistakes. Explore methods like copy(), set(), and deepcopy().
PythonDifferent Methods to Loop Through Sets in Python (Beginner-Friendly Guide)
Learn diverse methods to loop through sets in Python, including practical examples and use cases, ideal for beginners looking to master set operations.
