Python Projects for Beginners
The best first Python projects are small programs that do one useful thing end to end: a number-guessing game, a word counter, a unit converter, a password generator and a contact book that saves to a file. Each one takes under an hour and teaches a distinct skill — input handling, string work, dictionaries, standard-library modules and file persistence.
Every snippet below was executed before publishing, including the parts where Python does something surprising.
Most beginner project lists give you a title and leave you to it. This one gives you working code for five projects, explains the one idea each project exists to teach, and points out the specific mistake beginners make in each. Work through them in order; each builds on the last.
You will need Python installed — our guide to installing Python on Windows covers that, including the py launcher used in the commands here.
Project 1: number guessing game
Teaches: loops, conditionals, and handling input that is not what you expected.
The naive version is five lines and crashes the moment someone types "abc". The part worth writing is the validation:
import random
def parse_guess(raw, lo=1, hi=100):
"""Return (number, None) or (None, reason)."""
raw = raw.strip()
if not raw:
return None, "Type a number."
try:
n = int(raw)
except ValueError:
return None, f"{raw!r} is not a whole number."
if not lo <= n <= hi:
return None, f"{n} is outside {lo}-{hi}."
return n, None
secret = random.randint(1, 100)
tries = 0
while True:
guess, problem = parse_guess(input("Guess (1-100): "))
if problem:
print(problem)
continue
tries += 1
if guess < secret:
print("Higher.")
elif guess > secret:
print("Lower.")
else:
print(f"Got it in {tries} tries.")
break
Running parse_guess against awkward input shows why it exists:
'42' -> (42, None)
' 7 ' -> (7, None)
'abc' -> (None, "'abc' is not a whole number.")
'' -> (None, 'Type a number.')
'999' -> (None, '999 is outside 1-100.')
'3.5' -> (None, "'3.5' is not a whole number.")
The beginner mistake: using if raw.isdigit() instead of try/except. isdigit() rejects negative numbers and returns True for some non-ASCII digit characters. Catching ValueError around int() is the idiomatic way, and it is the same pattern you will use for every user input you ever parse.
Project 2: word counter
Teaches: string methods, dictionaries, and returning structured data instead of printing from inside a function.
def count_text(text):
words = text.split()
sentences = sum(text.count(c) for c in ".!?")
return {
"words": len(words),
"chars": len(text),
"chars_no_spaces": len(text.replace(" ", "").replace("\n", "")),
"sentences": sentences,
"reading_minutes": max(1, round(len(words) / 220)) if words else 0,
}
sample = "Python is readable. It is also practical! Is it perfect? No."
print(count_text(sample))
{'words': 11, 'chars': 60, 'chars_no_spaces': 50,
'sentences': 4, 'reading_minutes': 1}
The beginner mistake: using text.split(" ") rather than text.split(). The bare version splits on any run of whitespace and discards empties, so double spaces, tabs and newlines are handled correctly. split(" ") turns "a b" into three items, one of them empty.
The 220 words-per-minute figure is a common reading-speed estimate; our word count and reading time guide explains where such numbers come from, and the word counter tool does the same job in a browser.
Project 3: unit converter
Teaches: dictionaries as lookup tables, and the fact that floating-point arithmetic is approximate.
Rather than writing a function per pair of units, store a factor per unit relative to one base and convert through it:
FACTORS = {
"mm": 0.001, "cm": 0.01, "m": 1.0, "km": 1000.0,
"in": 0.0254, "ft": 0.3048, "mi": 1609.344,
}
def convert(value, frm, to):
if frm not in FACTORS or to not in FACTORS:
raise ValueError(f"unknown unit: {frm} or {to}")
return value * FACTORS[frm] / FACTORS[to]
Seven units, one function, and every direction works. Now run it and look closely:
convert(1, "mi", "km") -> 1.609344
convert(1, "in", "cm") -> 2.54
convert(5000, "mm", "m") -> 5.0
convert(1, "ft", "in") -> 12.000000000000002
That last line is real output. One foot is twelve inches, but Python prints 12.000000000000002, because 0.3048 and 0.0254 have no exact representation in binary floating point. This is not a Python bug — the same calculation in JavaScript gives the identical result, as our JavaScript unit converter guide shows.
Round only when you display, never in the stored value:
print(f"{convert(1, 'ft', 'in'):.10g}") # 12
The beginner mistake: rounding the factors instead of the output. Storing 1.609 rather than 1.609344 bakes an error into every conversion that no amount of display formatting can recover.
Project 4: password generator
Teaches: choosing the right standard-library module, which is a security decision more often than beginners realise.
import secrets
import string
def make_password(length=16):
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
return "".join(secrets.choice(alphabet) for _ in range(length))
print(make_password())
The beginner mistake, and it matters: using random.choice instead of secrets.choice. The random module is a Mersenne Twister, seeded predictably and designed for simulations, not secrecy — given enough output, its future values can be reconstructed. secrets uses the operating system's cryptographic random source. For anything that protects something, use secrets. This is the kind of distinction that separates code that looks right from code that is right.
Project 5: contact book that survives restarts
Teaches: reading and writing files, and JSON as a storage format.
import json
from pathlib import Path
STORE = Path("contacts.json")
def load():
if not STORE.exists():
return []
return json.loads(STORE.read_text(encoding="utf-8"))
def save(contacts):
STORE.write_text(json.dumps(contacts, indent=2), encoding="utf-8")
def add(name, email):
contacts = load()
contacts.append({"name": name, "email": email})
save(contacts)
return contacts
add("Ada", "ada@example.com")
print(load())
The beginner mistakes: forgetting encoding="utf-8", which on Windows can otherwise use a legacy code page and mangle any non-English name; and not handling the file-does-not-exist case, so the program crashes the very first time it runs. pathlib handles Windows and Unix paths without you writing backslashes by hand.
Where to take these next
| Project | Natural next step |
|---|---|
| Guessing game | Track best score across runs by saving it to a file |
| Word counter | Read a .txt file from the command line with sys.argv |
| Unit converter | Add weight and temperature — temperature needs offsets, not factors |
| Password generator | Guarantee at least one character from each class |
| Contact book | Add search and delete, then move to SQLite with sqlite3 |
Temperature is the interesting one. Every unit above is a pure multiple of a base, so a single factor works. Celsius and Fahrenheit have an offset as well as a scale, so value * factor cannot express them and you need a pair of functions per unit instead. Discovering that your neat abstraction assumed something it never stated is a genuinely useful lesson.
Frequently asked questions
What are good Python projects for complete beginners?
Small programs that do one thing end to end: a number-guessing game, a word counter, a unit converter, a password generator and a file-backed contact book. Each takes under an hour, and together they cover input validation, string handling, dictionaries, standard-library modules and file persistence.
How long should a beginner Python project take?
Aim for something you can finish in one sitting, roughly 30 to 60 minutes. Finishing five small programs teaches far more than abandoning one ambitious one, because you practise the whole cycle — write, run, hit an error, fix it — five times instead of getting stuck once.
Why does my Python unit conversion print 12.000000000000002?
Because floating-point numbers are stored in binary and decimals like 0.3048 have no exact binary representation. The arithmetic is correct to about 15 to 17 significant digits, and the remainder is display noise. Format the result when you print it, for example f"{value:.10g}", and leave the stored number alone.
Should I use random or secrets for passwords?
Always secrets. The random module uses a Mersenne Twister designed for simulations; its output is predictable given enough samples. secrets draws on the operating system's cryptographic random source and exists specifically for tokens and passwords.
Do I need a virtual environment for these projects?
Not for these five, since they only use the standard library. Get into the habit anyway — run py -m venv .venv in each project folder — because the first time you install a package you will want the isolation, and it costs nothing to start early.
Conclusion
These five projects are small on purpose. Each one is finishable, each teaches one idea properly, and three of them contain a mistake that is easy to make and easy to miss — isdigit() over try/except, split(" ") over split(), and random over secrets.
Type the code rather than pasting it, break it deliberately, and read the error messages. That is the whole method.
Comments