🐍

Python Cheat Sheet

Beginner basics - everything from your first print() to strings and f-strings. Copy-paste the examples and learn by doing.

Jump to: First programprint()VariablesData types InputType conversionMathStrings Slicingf-stringsString methods Comparisonif / elif / elseLogical ops While loopsFor loopsLists List methods2D listsTuples UnpackingDictionariesFunctions CommentsClassesConstructors InheritanceModulesPackagesRandom Format specifiersDefault args*args / **kwargs ScopeComprehensionsSets ExceptionsFilesmatch-caseDunder methods Installing librariesExcel automationMachine learning 🚀 Projects

Your first program

Save a file as app.py, then run it. Python runs your code line by line, from the top.

print("Hello, world!")
print("My name is Mosh")

print()

print("Hello")        # text (a "string")
print("*" * 10)        # ********** (repeat a string)
print("A", "B", "C")  # A B C   (multiple values)

Variables

A variable is a labelled box that stores a value in memory. = assigns a value.

price = 10          # create / set
price = 20          # reset to a new value
print(price)        # 20
📝 Naming: use lowercase, separate words with _ (e.g. first_name), and pick descriptive names. Python is case-sensitive (Priceprice).

Data types

TypeExampleWhat it is
intage = 20Whole number
floatrating = 4.9Number with a decimal
strname = "Mosh"Text (a string)
boolis_new = TrueTrue / False (capital T/F!)
name = "Mosh"
age = 20
is_new = True
print(type(age))   # <class 'int'> - check a type

Receiving input

input() shows a prompt and returns whatever the user types - always as a string.

name = input("What is your name? ")
print("Hi " + name)   # join strings with +

Type conversion

Because input() gives a string, convert it before doing math.

birth_year = input("Birth year: ")
age = 2026 - int(birth_year)  # int() turns "1982" into 1982
print(age)
FunctionConverts to
int(x)integer
float(x)decimal number
str(x)string
bool(x)True / False
⚠️ "1982" (string) is not the same as 1982 (number). Mixing them gives a TypeError - convert first.

Math operators

OpMeaningExample
+ - * /add, subtract, multiply, divide10 / 3 → 3.33
//integer (floor) division10 // 3 → 3
%remainder (modulus)10 % 3 → 1
**power2 ** 3 → 8

Strings

Use single or double quotes. Use the other kind when your text contains a quote.

a = 'Python'
b = "Python's course"     # ' inside, so use "
c = 'He said "hi"'          # " inside, so use '
msg = """Hi John,
Thanks for joining.
The Team"""                 # triple quotes = multi-line

Indexing & slicing

Characters are numbered from 0. Negative numbers count from the end.

course = "Python for Beginners"
course[0]      # 'P'  (first char)
course[-1]     # 's'  (last char)
course[0:3]    # 'Pyt' (index 0,1,2 - stop is excluded)
course[1:]     # 'ython for Beginners' (to the end)
course[:5]     # 'Pytho' (from the start)
course[:]      # a full copy of the string
🎓 The [start:stop] slice includes start, excludes stop. This shows up on a lot of Python tests!

Formatted strings (f-strings)

Prefix with f and drop variables into { } - much cleaner than joining with +.

first = "John"
last = "Smith"
msg = f"{first} [{last}] is a coder"
print(msg)   # John [Smith] is a coder

String methods

Methods belong to a value and are called with a dot: course.upper(). They return a new string (the original is unchanged).

course = "Python for Beginners"
len(course)              # 20  (length - a general function)
course.upper()           # 'PYTHON FOR BEGINNERS'
course.lower()           # 'python for beginners'
course.title()           # 'Python For Beginners'
course.strip()           # remove spaces at the ends
course.find("o")         # 4   (index of first match, -1 if none)
course.replace("Beginners", "Pros")
"Python" in course      # True  (does it contain this?)

Comparison operators

These compare two values and produce a boolean (True/False).

OpMeansExample
==equal totemp == 30
!=not equalname != "Mosh"
> >=greater / or equaltemp > 30
< <=less / or equalage <= 18
⚠️ == compares, = assigns. temp = 30 sets a value; temp == 30 asks a question.

if / elif / else

Run code only when a condition is true. The indented block belongs to the if. elif = "otherwise if", else = "otherwise".

temp = 35
if temp > 30:
    print("It's a hot day")
    print("Drink water")
elif temp < 10:
    print("It's a cold day")
else:
    print("It's a lovely day")
# Example: down payment depends on credit
price = 1_000_000
has_good_credit = True
if has_good_credit:
    down = 0.1 * price          # 10%
else:
    down = 0.2 * price          # 20%
print(f"Down payment: ${down}")

Logical operators

Combine conditions: and (both true), or (at least one true), not (flips True↔False).

if has_high_income and has_good_credit:
    print("Eligible for a loan")      # both must be True

if has_high_income or has_good_credit:
    print("Eligible")               # at least one True

if has_good_credit and not has_criminal_record:
    print("Eligible")               # not False -> True

While loops

Repeat a block while a condition stays true. Always change something inside, or you get an infinite loop.

i = 1
while i <= 5:
    print(i)
    i += 1          # same as i = i + 1
print("Done")         # 1 2 3 4 5 Done

break jumps out of a loop early. A while … else runs the else only if the loop finished without a break:

secret = 9
guess_count = 0
guess_limit = 3
while guess_count < guess_limit:
    guess = int(input("Guess: "))
    guess_count += 1
    if guess == secret:
        print("You won!")
        break
else:
    print("Sorry, you failed")

For loops & range()

A for loop goes through each item in a collection (a string, a list, a range…).

for letter in "Python":
    print(letter)      # P y t h o n (each on a line)

for name in ["Mosh", "John", "Sarah"]:
    print(name)

for i in range(5):      # 0 1 2 3 4 (stop excluded)
    print(i)
range()Produces
range(5)0, 1, 2, 3, 4
range(5, 10)5, 6, 7, 8, 9
range(5, 10, 2)5, 7, 9 (step of 2)

Lists

A list holds many values in [ ]. Loop over it to process every item.

prices = [10, 20, 30]
total = 0
for price in prices:
    total += price
print(total)        # 60

Find the largest number

numbers = [3, 6, 2, 8, 4, 10]
max = numbers[0]
for n in numbers:
    if n > max:
        max = n
print(max)        # 10

List methods

Operations you can do on a list (call them with a dot):

numbers = [5, 2, 1, 7, 4]
numbers.append(20)        # add to the end
numbers.insert(0, 10)     # add at an index
numbers.remove(5)         # remove a value
numbers.pop()              # remove the last item
numbers.clear()           # remove everything
numbers.index(7)          # position of a value (error if missing)
7 in numbers              # True/False - safer existence check
numbers.count(5)          # how many times 5 appears
numbers.sort()            # sort ascending (in place)
numbers.reverse()         # reverse the order
copy = numbers.copy()     # an independent copy

Remove duplicates

numbers = [2, 2, 4, 6, 6, 3, 1]
uniques = []
for n in numbers:
    if n not in uniques:
        uniques.append(n)
print(uniques)     # [2, 4, 6, 3, 1]

2D lists (a list of lists)

matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]
matrix[0][1]            # 2  (row 0, column 1)
matrix[0][1] = 20       # change a cell
for row in matrix:      # nested loops
    for item in row:
        print(item)

Tuples

Like a list, but immutable - you can't add, remove or change items. Use ( ).

point = (1, 2, 3)
point[0]            # 1  (reading is fine)
point[0] = 10       # ❌ TypeError - tuples can't change
# only .count() and .index() are available

Unpacking

Assign several variables from a list/tuple in one line.

coordinates = (1, 2, 3)
x, y, z = coordinates    # x=1, y=2, z=3
print(x, y, z)

Dictionaries

Store key → value pairs in { }. Keys must be unique.

customer = {
    "name": "John Smith",
    "age": 30,
    "is_verified": True,
}
customer["name"]                 # 'John Smith'
customer.get("birthdate")         # None if missing (no error)
customer.get("birthdate", "N/A")  # a default value
customer["name"] = "Jack"        # update
customer["phone"] = "1234"        # add a new pair
💡 dict[key] errors if the key is missing; dict.get(key, default) is safer. "good morning :)".split(" ") turns a string into a list of words - handy with dictionaries (emoji converters, phone-number spellers, etc.).

Functions

Group reusable code with def. Define before you call. Parameters are the placeholders; arguments are the values you pass.

def greet_user(first_name, last_name):
    print(f"Hi {first_name} {last_name}")
    print("Welcome aboard")

greet_user("John", "Smith")              # positional args
greet_user(last_name="Smith", first_name="John")  # keyword args (order-free)

Return a value

Use return to send a result back. A function with no return gives None.

def square(number):
    return number * number

result = square(3)
print(result)      # 9
📌 Rules: keyword arguments must come after positional ones. Add two blank lines after a function (PEP 8 style). Use descriptive names like calculate_cost.

Comments

Lines starting with # are ignored by Python. Use them to explain why (not what) - and don't overdo it.

# Tax rate assumed at 10% for 2026
price = 100  # base price before tax

Classes & objects

A class defines a new type (a blueprint). An object is an instance of it. Class names use PascalCase. Every method's first parameter is self (the current object).

class Point:
    def move(self):
        print("move")
    def draw(self):
        print("draw")

point1 = Point()        # create an object (instance)
point1.draw()           # call a method
point1.x = 10          # attributes = data on the object
print(point1.x)        # 10

Constructors (__init__)

__init__ runs automatically when you create an object - use it to set up (initialize) attributes so they always exist.

class Person:
    def __init__(self, name):
        self.name = name           # self = this object
    def talk(self):
        print(f"Hi, I am {self.name}")

john = Person("John Smith")     # name is passed to __init__
john.talk()                    # Hi, I am John Smith

Inheritance

A class can reuse another class's methods by inheriting from it - avoids repeating code (DRY). Use pass for an empty body.

class Mammal:
    def walk(self):
        print("walk")

class Dog(Mammal):     # Dog inherits walk()
    def bark(self):
        print("bark")

class Cat(Mammal):     # Cat inherits walk() too
    pass

dog1 = Dog()
dog1.walk()              # inherited
dog1.bark()              # Dog's own method

Modules

A module is just a .py file. Split related functions/classes into modules, then import them.

# converters.py has kg_to_lbs()
import converters
converters.kg_to_lbs(70)

# or import just what you need:
from converters import kg_to_lbs
kg_to_lbs(70)          # no prefix needed

Packages

A package is a folder of modules (it contains an __init__.py file). Import using dots.

# ecommerce/shipping.py has calculate_shipping()
import ecommerce.shipping
ecommerce.shipping.calculate_shipping()

from ecommerce.shipping import calculate_shipping
from ecommerce import shipping   # import the whole module

Random values (a built-in module)

Python ships with a big standard library. random is one example - no install needed.

import random
random.random()             # a float 0.0-1.0
random.randint(1, 6)         # whole number 1-6 (like a die)
random.choice(["Jon", "Mary", "Bob"])  # pick a random item
# Dice class that rolls two dice -> a tuple
import random
class Dice:
    def roll(self):
        return random.randint(1, 6), random.randint(1, 6)

dice = Dice()
print(dice.roll())      # e.g. (3, 5)

Format specifiers (pretty f-strings)

Inside an f-string, add : and a format spec to control how a value looks.

pi = 3.14159
name = "Mosh"
f"{pi:.2f}"          # '3.14'   (2 decimal places)
f"{1000000:,}"      # '1,000,000'  (thousands commas)
f"{0.25:.0%}"       # '25%'    (percent)
f"{name:>10}"       # right-align in 10 spaces
f"{name:^10}"       # center-align

Default & keyword arguments

Give a parameter a default so callers can skip it. Pass by name for clarity.

def greet(name, greeting="Hello"):   # greeting has a default
    print(f"{greeting}, {name}")

greet("Mosh")                 # Hello, Mosh
greet("Mosh", "Hi")           # Hi, Mosh
greet(greeting="Hey", name="Bob")  # keyword args (any order)

*args & **kwargs (any number of arguments)

def add(*numbers):       # *args  -> a tuple of all positional args
    return sum(numbers)
add(1, 2, 3, 4)          # 10

def profile(**info):     # **kwargs -> a dict of keyword args
    print(info)
profile(name="Mosh", age=30)  # {'name': 'Mosh', 'age': 30}

Variable scope

Variables made inside a function are local (gone when it ends). Use global to change a top-level variable from inside a function.

total = 0              # global
def add():
    global total      # without this you'd make a NEW local 'total'
    total += 1

List comprehensions

A short way to build a list from another sequence - one line instead of a loop.

nums = [1, 2, 3, 4, 5]
squares = [n * n for n in nums]            # [1, 4, 9, 16, 25]
evens = [n for n in nums if n % 2 == 0]    # [2, 4]

Sets

An unordered collection of unique items, in { }. Great for removing duplicates.

s = {1, 2, 3, 3, 2}       # {1, 2, 3}  (dupes dropped)
s.add(4)
{1, 2, 3} & {2, 3, 4}     # {2, 3}     intersection
{1, 2} | {3, 4}        # {1,2,3,4}  union
{1, 2, 3} - {2}        # {1, 3}     difference
set([1, 1, 2])           # {1, 2}  remove duplicates from a list

Exception handling (try / except)

Catch errors so your program doesn't crash. else runs if there was no error; finally always runs.

try:
    age = int(input("Age: "))
    print(100 / age)
except ValueError:
    print("Please enter a number")
except ZeroDivisionError:
    print("Age can't be zero")
else:
    print("No errors!")
finally:
    print("Done")             # always runs

File handling

Use with open(...) - it closes the file for you. Modes: "r" read, "w" write (overwrites), "a" append.

with open("notes.txt", "w") as f:
    f.write("Hello\n")
    f.write("Second line\n")

with open("notes.txt", "r") as f:
    content = f.read()        # whole file as one string
    # for line in f:  print(line)   # or line by line

match-case (Python 3.10+)

A clean alternative to a long if / elif chain.

command = "start"
match command:
    case "start":
        print("Starting…")
    case "stop":
        print("Stopping…")
    case _:                # _ = anything else (default)
        print("Unknown command")

Magic / dunder methods

Special methods named with double underscores let your objects work with built-in syntax (printing, ==, etc.).

class Point:
    def __init__(self, x):
        self.x = x
    def __str__(self):           # controls print(p)
        return f"Point({self.x})"
    def __eq__(self, other):      # controls p1 == p2
        return self.x == other.x

p = Point(5)
print(p)               # Point(5)
print(p == Point(5))    # True

Installing third-party libraries

Beyond the standard library, install packages from PyPI with pip (run in your terminal, not in Python).

pip install openpyxl
pip install pandas scikit-learn jupyter
💡 For data/ML work, Anaconda bundles Python + Jupyter + pandas/numpy/scikit-learn so you don't install them one by one. You write ML code in a Jupyter Notebook (cells you run one at a time) - great for inspecting data.

Project: automate Excel (openpyxl)

Read a spreadsheet, change values, and save - perfect for boring repetitive tasks across thousands of files.

import openpyxl as xl
from openpyxl.chart import BarChart, Reference

wb = xl.load_workbook("transactions.xlsx")
sheet = wb.active                 # or wb["Sheet1"]

cell = sheet.cell(1, 1)          # row 1, col 1  (or sheet["a1"])
print(cell.value)

for row in range(2, sheet.max_row + 1):   # skip header row 1
    price = sheet.cell(row, 3).value
    corrected = price * 0.9           # 10% off
    sheet.cell(row, 4).value = corrected  # write into a new column

wb.save("transactions2.xlsx")

Add a chart

values = Reference(sheet, min_row=2, max_row=sheet.max_row, min_col=4, max_col=4)
chart = BarChart()
chart.add_data(values)
sheet.add_chart(chart, "e2")        # top-left corner of the chart
🧹 Pro tip from the course: wrap it in a process_workbook(filename) function, then loop over every file in a folder to update thousands of spreadsheets in seconds.

Project: machine learning (pandas + scikit-learn)

The ML workflow: import → prepare → train → predict. Example: predict the music genre someone likes from their age & gender.

import pandas as pd
from sklearn.tree import DecisionTreeClassifier

# 1. import data (a CSV -> a DataFrame, like a spreadsheet)
music = pd.read_csv("music.csv")
music.shape          # (rows, columns)
music.describe()     # quick stats per column

# 2. prepare: split into input (X) and output (y)
X = music.drop(columns=["genre"])   # everything except the answer
y = music["genre"]                  # the answer column

# 3. build & train a model
model = DecisionTreeClassifier()
model.fit(X, y)

# 4. predict (21-yr-old male, 22-yr-old female)
predictions = model.predict([[21, 1], [22, 0]])
print(predictions)   # e.g. ['HipHop' 'Dance']
📊 Common ML libraries: pandas (data frames), numpy (arrays), matplotlib (plots), scikit-learn (algorithms like decision trees). Always split data into a training set and a testing set, then measure your model's accuracy.

🚀 Projects - build real things

Classic beginner-to-advanced projects (the ones from the big YouTube project courses), rewritten clean. One project of each level is free; reveal the rest with ⭐ credits, or unlock the whole Projects section.

Number Guessing Game EASY

Build: the computer picks 1-100; you guess until you get it, with hot/cold hints.

import random

secret = random.randint(1, 100)
guesses = 0
print("I'm thinking of a number between 1 and 100.")

while True:
    guess = int(input("Your guess: "))
    guesses += 1
    if guess < secret:
        print("Too low!")
    elif guess > secret:
        print("Too high!")
    else:
        print(f"You got it in {guesses} guesses!")
        break

Rock Paper Scissors EASY

Build: play against the computer; a dict decides who beats whom.

import random

options = ["rock", "paper", "scissors"]
beats = {"rock": "scissors", "paper": "rock", "scissors": "paper"}

you = input("rock, paper or scissors? ").lower()
cpu = random.choice(options)
print(f"Computer chose {cpu}.")

if you == cpu:
    print("Tie!")
elif beats.get(you) == cpu:
    print("You win!")
else:
    print("You lose!")

Password Generator EASY

Build: a random strong password of any length from letters, digits and symbols.

import random, string

length = int(input("Password length: "))
chars = string.ascii_letters + string.digits + "!@#$%^&*"
password = "".join(random.choice(chars) for _ in range(length))
print("Your password:", password)

Mad Libs EASY

Build: collect random words from the user, then drop them into a story.

noun = input("A noun: ")
verb = input("A verb (past tense): ")
adj = input("An adjective: ")
place = input("A place: ")

print(f"Yesterday I went to the {place} and saw a {adj} {noun}.")
print(f"It suddenly {verb} right in front of me!")

Dice Roller EASY

Build: roll two dice on demand; doubles let you roll again.

import random

while input("Roll the dice? (y/n) ").lower() == "y":
    die1 = random.randint(1, 6)
    die2 = random.randint(1, 6)
    print(f"You rolled {die1} and {die2} = {die1 + die2}")
    if die1 == die2:
        print("Doubles! Roll again.")

Temperature Converter EASY

Build: convert between Celsius and Fahrenheit.

temp = float(input("Temperature: "))
unit = input("Is that (C)elsius or (F)ahrenheit? ").upper()

if unit == "C":
    print(f"{temp * 9/5 + 32:.1f} F")
elif unit == "F":
    print(f"{(temp - 32) * 5/9:.1f} C")
else:
    print("Unknown unit.")

Calculator EASY

Build: a four-function calculator with divide-by-zero handling.

a = float(input("First number: "))
op = input("Operator (+ - * /): ")
b = float(input("Second number: "))

if op == "+":
    print(a + b)
elif op == "-":
    print(a - b)
elif op == "*":
    print(a * b)
elif op == "/":
    print(a / b if b != 0 else "can't divide by zero")
else:
    print("Unknown operator")

Countdown Timer EASY

Build: a ticking MM:SS countdown to zero.

import time

seconds = int(input("Count down from how many seconds? "))
while seconds > 0:
    mins, secs = divmod(seconds, 60)
    print(f"{mins:02d}:{secs:02d}", end="\r")
    time.sleep(1)
    seconds -= 1
print("Time's up! ⏰   ")

Currency Converter EASY

Build: convert between currencies using a rate table (convert via USD).

rates = {"USD": 1.0, "EUR": 0.92, "GBP": 0.79, "JPY": 156.0, "CAD": 1.37}

amount = float(input("Amount: "))
src = input("From (USD/EUR/GBP/JPY/CAD): ").upper()
dst = input("To: ").upper()

usd = amount / rates[src]          # normalise to USD first
converted = usd * rates[dst]
print(f"{amount} {src} = {converted:.2f} {dst}")

Turtle Spiral (graphics) EASY

Build: a colourful spiral with Python's turtle. Opens a drawing window - won't run in the browser.

import turtle

t = turtle.Turtle()
t.speed(0)
colors = ["red", "orange", "yellow", "green", "blue", "purple"]

for i in range(180):
    t.color(colors[i % len(colors)])
    t.forward(i * 2)
    t.left(59)

turtle.done()

Weight Converter EASY

Build: convert between kilograms and pounds.

weight = float(input("Weight: "))
unit = input("Is that (K)g or (L)bs? ").upper()

if unit == "K":
    print(f"{weight * 2.20462:.1f} lbs")
elif unit == "L":
    print(f"{weight / 2.20462:.1f} kg")
else:
    print("Unknown unit.")

Stopwatch EASY

Build: start, then stop - and see how much time passed.

import time

input("Press Enter to start the stopwatch…")
start = time.time()
input("Press Enter again to stop…")
elapsed = time.time() - start
print(f"Elapsed: {elapsed:.2f} seconds")

Tip Calculator & Bill Splitter EASY

Build: enter the bill, tip % and group size - get the tip, total, and each person's share.

bill = float(input("Bill amount: $"))
tip_pct = float(input("Tip percent: "))
people = int(input("How many people? "))

tip = bill * tip_pct / 100
total = bill + tip
share = total / people

print(f"Tip:   ${tip:.2f}")
print(f"Total: ${total:.2f}")
print(f"Each person pays ${share:.2f}")

BMI Calculator EASY

Build: weight + height in, BMI + category out - a first taste of if/elif chains.

weight = float(input("Weight (kg): "))
height = float(input("Height (m): "))

bmi = weight / height ** 2

if bmi < 18.5:
    category = "underweight"
elif bmi < 25:
    category = "healthy"
elif bmi < 30:
    category = "overweight"
else:
    category = "obese"

print(f"BMI: {bmi:.1f} ({category})")

FizzBuzz EASY

Build: the classic warm-up: multiples of 3 say Fizz, of 5 say Buzz, of both say FizzBuzz.

n = int(input("Count up to: "))

for i in range(1, n + 1):
    if i % 15 == 0:
        print("FizzBuzz")
    elif i % 3 == 0:
        print("Fizz")
    elif i % 5 == 0:
        print("Buzz")
    else:
        print(i)

Palindrome Checker EASY

Build: ignores spaces, punctuation and case - 'A man, a plan, a canal: Panama' passes.

text = input("Enter text: ")

cleaned = "".join(c.lower() for c in text if c.isalnum())

if cleaned == cleaned[::-1]:
    print("That's a palindrome!")
else:
    print("Not a palindrome.")

Multiplication Table EASY

Build: print a neat, right-aligned times table for any number.

n = int(input("Which table? "))

for i in range(1, 11):
    print(f"{n} x {i:2d} = {n * i:3d}")

Leap Year Checker EASY

Build: the real rule (divisible by 4, except centuries unless divisible by 400).

year = int(input("Year: "))

if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0):
    print(f"{year} is a leap year!")
else:
    print(f"{year} is not a leap year.")

Days Until Your Birthday EASY

Build: enter your birthday as MM-DD and count down to it with the datetime module.

from datetime import date

month, day = map(int, input("Birthday (MM-DD): ").split("-"))

today = date.today()
birthday = date(today.year, month, day)
if birthday < today:
    birthday = date(today.year + 1, month, day)

days = (birthday - today).days
if days == 0:
    print("Happy birthday - it's today! \U0001f382")
else:
    print(f"{days} days until your birthday!")

Acronym Generator EASY

Build: turn any phrase into its initials - 'random access memory' becomes RAM.

phrase = input("Enter a phrase: ")

acronym = "".join(word[0].upper() for word in phrase.split())
print(f"Acronym: {acronym}")

Word & Character Counter EASY

Build: instant text stats: words, characters, letters and the longest word.

text = input("Enter some text: ")

words = text.split()
letters = sum(1 for c in text if c.isalpha())

print(f"Words:      {len(words)}")
print(f"Characters: {len(text)}")
print(f"Letters:    {letters}")
if words:
    print(f"Longest word: {max(words, key=len)}")

Coin Flip Simulator EASY

Build: flip 1000 virtual coins, then report the totals and the longest streak.

import random

heads = tails = 0
streak = best_streak = 0
last = None

for _ in range(1000):
    flip = random.choice(["H", "T"])
    if flip == "H":
        heads += 1
    else:
        tails += 1
    if flip == last:
        streak += 1
    else:
        streak = 1
        last = flip
    best_streak = max(best_streak, streak)

print(f"Heads: {heads}   Tails: {tails}")
print(f"Longest streak of the same side: {best_streak}")

Roman Numeral Converter EASY

Build: convert 1994 to MCMXCIV - then convert it right back to prove it works.

VALUES = [(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"),
          (50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I")]

def to_roman(n):
    out = ""
    for value, symbol in VALUES:
        while n >= value:
            out += symbol
            n -= value
    return out

def from_roman(s):
    single = {"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000}
    total = 0
    for i, c in enumerate(s):
        if i + 1 < len(s) and single[c] < single[s[i + 1]]:
            total -= single[c]
        else:
            total += single[c]
    return total

n = int(input("Number (1-3999): "))
roman = to_roman(n)
print(f"{n} = {roman}")
print(f"...and back: {from_roman(roman)}")

Decimal ↔ Binary Converter EASY

Build: see the same number in binary, octal and hex - then decode a binary string back.

n = int(input("Decimal number: "))
print(f"Binary: {bin(n)[2:]}")
print(f"Octal:  {oct(n)[2:]}")
print(f"Hex:    {hex(n)[2:].upper()}")

b = input("Now give me a binary string: ")
print(f"{b} in decimal is {int(b, 2)}")

Loan Payment Calculator EASY

Build: the real amortization formula banks use - see the true cost of a mortgage.

principal = float(input("Loan amount: $"))
annual_rate = float(input("Annual interest rate %: "))
years = int(input("Years: "))

r = annual_rate / 100 / 12          # monthly rate
n = years * 12                       # number of payments

if r == 0:
    monthly = principal / n
else:
    monthly = principal * r * (1 + r) ** n / ((1 + r) ** n - 1)

total = monthly * n
print(f"Monthly payment: ${monthly:,.2f}")
print(f"Total paid:      ${total:,.2f}")
print(f"Total interest:  ${total - principal:,.2f}")

GPA Calculator EASY

Build: type letter grades until you press Enter on a blank line - get your GPA.

POINTS = {"A": 4.0, "A-": 3.7, "B+": 3.3, "B": 3.0, "B-": 2.7,
          "C+": 2.3, "C": 2.0, "C-": 1.7, "D": 1.0, "F": 0.0}

grades = []
while True:
    g = input("Grade (blank to finish): ").strip().upper()
    if not g:
        break
    if g in POINTS:
        grades.append(POINTS[g])
    else:
        print("Unknown grade, try again.")

if grades:
    print(f"GPA: {sum(grades) / len(grades):.2f}")
else:
    print("No grades entered.")

12-Hour ↔ 24-Hour Time Converter EASY

Build: type 14:30 to get 2:30 PM, or 2:30 PM to get 14:30 - strptime does the parsing.

from datetime import datetime

t = input("Enter a time (14:30 or 2:30 PM): ").strip()

try:
    parsed = datetime.strptime(t, "%H:%M")
    print(parsed.strftime("%I:%M %p").lstrip("0"))
except ValueError:
    parsed = datetime.strptime(t.upper(), "%I:%M %p")
    print(parsed.strftime("%H:%M"))

Anagram Checker EASY

Build: do two words use exactly the same letters? sorted() makes it a one-liner.

a = input("First word:  ").lower().replace(" ", "")
b = input("Second word: ").lower().replace(" ", "")

if sorted(a) == sorted(b):
    print("Anagrams!")
else:
    print("Not anagrams.")

Dice Statistics EASY

Build: roll two dice 10,000 times and draw a text histogram - watch the bell curve appear.

import random

counts = {total: 0 for total in range(2, 13)}
for _ in range(10000):
    roll = random.randint(1, 6) + random.randint(1, 6)
    counts[roll] += 1

for total in range(2, 13):
    bar = "#" * (counts[total] // 40)
    print(f"{total:2d} | {bar} {counts[total]}")

Piggy Bank EASY

Build: a tiny savings tracker: add money, check the balance, quit when you're done.

balance = 0.0
print("Commands: add <amount>, balance, quit")

while True:
    cmd = input("> ").strip().lower()
    if cmd == "quit":
        print(f"Final balance: ${balance:.2f} - keep saving!")
        break
    elif cmd == "balance":
        print(f"You have ${balance:.2f}")
    elif cmd.startswith("add "):
        try:
            balance += float(cmd.split()[1])
            print(f"Added! New balance: ${balance:.2f}")
        except ValueError:
            print("Usage: add 5.00")
    else:
        print("Unknown command.")

Hangman MEDIUM

Build: guess the hidden word letter by letter before you run out of lives.

import random

words = ["python", "rocket", "galaxy", "wizard", "dragon"]
word = random.choice(words)
guessed = set()
lives = 6

while lives > 0:
    shown = "".join(c if c in guessed else "_" for c in word)
    print(shown, f"  (lives: {lives})")
    if "_" not in shown:
        print("You won!")
        break
    letter = input("Guess a letter: ").lower()
    if letter in word:
        guessed.add(letter)
    else:
        lives -= 1
        print("Nope!")
else:
    print(f"Game over - the word was '{word}'.")

Quiz Game MEDIUM

Build: ask a list of questions, score the answers, show the result.

questions = [
    ("What keyword defines a function? ", "def"),
    ("What type is 3.14? ", "float"),
    ("What does len() return? ", "length"),
]
score = 0
for question, answer in questions:
    if input(question).strip().lower() == answer:
        print("Correct!")
        score += 1
    else:
        print(f"Wrong - it's '{answer}'.")
print(f"You scored {score}/{len(questions)}.")

Tic-Tac-Toe (2 players) MEDIUM

Build: a 3×3 board, alternating X/O, with win detection.

board = [" "] * 9
LINES = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)]

def show():
    for r in range(0, 9, 3):
        print(" " + " | ".join(board[r:r+3]))

def winner():
    for a, b, c in LINES:
        if board[a] == board[b] == board[c] != " ":
            return board[a]
    return None

player = "X"
for _ in range(9):
    show()
    i = int(input(f"{player}, pick 0-8: "))
    if board[i] == " ":
        board[i] = player
        if winner():
            show(); print(f"{player} wins!"); break
        player = "O" if player == "X" else "X"
else:
    print("It's a draw!")

Slot Machine MEDIUM

Build: a betting slot machine - match symbols to win, lose your bet otherwise.

import random

balance = 100
SYMBOLS = ["cherry", "lemon", "bell", "star", "seven"]

while balance > 0:
    bet = int(input(f"Balance ${balance}. Bet how much? "))
    if bet > balance or bet <= 0:
        print("Invalid bet.")
        continue
    row = [random.choice(SYMBOLS) for _ in range(3)]
    print(" | ".join(row))
    if row[0] == row[1] == row[2]:
        balance += bet * 5
        print(f"JACKPOT! +${bet * 5}")
    elif len(set(row)) == 2:
        balance += bet
        print(f"Two match! +${bet}")
    else:
        balance -= bet
        print(f"No match. -${bet}")
print("Game over - out of money.")

Caesar Cipher (encryption) MEDIUM

Build: shift each letter to encrypt a message - and shift back to decrypt.

def caesar(text, shift):
    out = ""
    for ch in text:
        if ch.isalpha():
            base = ord("A") if ch.isupper() else ord("a")
            out += chr((ord(ch) - base + shift) % 26 + base)
        else:
            out += ch
    return out

message = input("Message: ")
key = int(input("Shift by: "))
encrypted = caesar(message, key)
print("Encrypted:", encrypted)
print("Decrypted:", caesar(encrypted, -key))

To-Do List (saved to a file) MEDIUM

Build: add/complete/list tasks that persist to a JSON file between runs.

import json, os

FILE = "todos.json"
todos = json.load(open(FILE)) if os.path.exists(FILE) else []

while True:
    cmd = input("[a]dd  [d]one  [l]ist  [q]uit: ").lower()
    if cmd == "a":
        todos.append({"task": input("Task: "), "done": False})
    elif cmd == "d":
        i = int(input("Number done: ")) - 1
        if 0 <= i < len(todos):
            todos[i]["done"] = True
    elif cmd == "l":
        for i, t in enumerate(todos, 1):
            mark = "x" if t["done"] else " "
            print(f"{i}. [{mark}] {t['task']}")
    elif cmd == "q":
        json.dump(todos, open(FILE, "w"))
        break

The Computer Guesses Your Number MEDIUM

Build: you think of a number; the computer finds it with binary search.

print("Think of a number between 1 and 100. I'll guess it.")
low, high = 1, 100

while low <= high:
    guess = (low + high) // 2
    hint = input(f"Is it {guess}? (h)igher / (l)ower / (c)orrect: ").lower()
    if hint == "c":
        print(f"Got it in - the number is {guess}!")
        break
    elif hint == "h":
        low = guess + 1
    elif hint == "l":
        high = guess - 1

Binary Search MEDIUM

Build: the classic O(log n) search - halve the range each step.

def binary_search(arr, target):
    low, high = 0, len(arr) - 1
    while low <= high:
        mid = (low + high) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return -1

nums = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
print(binary_search(nums, 23))   # 5
print(binary_search(nums, 7))    # -1 (not found)

Alarm Clock MEDIUM

Build: wait until a set time, then sound the alarm.

import time
from datetime import datetime

alarm = input("Set alarm (HH:MM, 24-hour): ")
print("Waiting…")
while True:
    now = datetime.now().strftime("%H:%M")
    if now == alarm:
        print("⏰ Wake up!")
        break
    time.sleep(10)

Password Manager MEDIUM

Build: save and look up site passwords in a file (lightly scrambled).

import json, os

FILE = "vault.json"
vault = json.load(open(FILE)) if os.path.exists(FILE) else {}

def scramble(text, k):              # demo only - use real crypto for real apps
    return "".join(chr(ord(c) + k) for c in text)

while True:
    cmd = input("[s]ave  [g]et  [q]uit: ").lower()
    if cmd == "s":
        site = input("Site: ")
        vault[site] = scramble(input("Password: "), 5)
        json.dump(vault, open(FILE, "w"))
    elif cmd == "g":
        site = input("Site: ")
        print("Password:", scramble(vault[site], -5) if site in vault else "(not found)")
    elif cmd == "q":
        break

Email Sender MEDIUM

Build: send an email from a script with smtplib (use a Gmail App Password).

import smtplib
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
msg["Subject"] = "Hello from Python!"
msg.set_content("This email was sent by a Python script.")

with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
    server.login("[email protected]", "your-app-password")
    server.send_message(msg)
print("Email sent!")

Digital Clock (tkinter GUI) MEDIUM

Build: a live ticking clock in a window with tkinter. Opens a window - won't run in the browser.

import tkinter as tk
from time import strftime

root = tk.Tk()
root.title("Clock")

label = tk.Label(root, font=("Consolas", 48), bg="black", fg="cyan")
label.pack(padx=40, pady=20)

def tick():
    label.config(text=strftime("%H:%M:%S"))
    label.after(1000, tick)

tick()
root.mainloop()

Paint App (tkinter GUI) MEDIUM

Build: drag the mouse to draw on a canvas. Opens a window - won't run in the browser.

import tkinter as tk

root = tk.Tk()
root.title("Paint")
canvas = tk.Canvas(root, width=500, height=400, bg="white")
canvas.pack()

def draw(event):
    x, y = event.x, event.y
    canvas.create_oval(x - 3, y - 3, x + 3, y + 3, fill="black", outline="black")

canvas.bind("<B1-Motion>", draw)      # draw while the left button is held
root.mainloop()

Typing Speed Test MEDIUM

Build: time how fast the user types a sentence and report words-per-minute.

import time

sentence = "the quick brown fox jumps over the lazy dog"
print("Type this as fast as you can:")
print(sentence)
input("Press Enter to start…")

start = time.time()
typed = input("> ")
elapsed = time.time() - start

words = len(sentence.split())
wpm = words / (elapsed / 60)
ok = typed.strip() == sentence
print(f"Time {elapsed:.1f}s | {wpm:.0f} WPM | {'Perfect!' if ok else 'Some typos.'}")

Shopping Cart MEDIUM

Build: add items with prices, then print an itemised receipt with the total.

foods = []
prices = []

while True:
    food = input("Add an item (or 'q' to check out): ")
    if food.lower() == "q":
        break
    price = float(input(f"Price of {food}: $"))
    foods.append(food)
    prices.append(price)

print("----- YOUR CART -----")
for food, price in zip(foods, prices):
    print(f"{food:12} ${price:.2f}")
print(f"TOTAL: ${sum(prices):.2f}")

Contact Book (saved to JSON) MEDIUM

Build: add, list, search and delete contacts - everything persists to contacts.json between runs.

import json, os

FILE = "contacts.json"
contacts = json.load(open(FILE)) if os.path.exists(FILE) else []

def save():
    json.dump(contacts, open(FILE, "w"), indent=2)

while True:
    print("\n1) Add  2) List  3) Search  4) Delete  5) Quit")
    choice = input("> ").strip()
    if choice == "1":
        contacts.append({"name": input("Name: "), "phone": input("Phone: ")})
        save()
        print("Saved!")
    elif choice == "2":
        for i, c in enumerate(contacts, 1):
            print(f"{i}. {c['name']} - {c['phone']}")
        if not contacts:
            print("(empty)")
    elif choice == "3":
        q = input("Search: ").lower()
        for c in contacts:
            if q in c["name"].lower():
                print(f"{c['name']} - {c['phone']}")
    elif choice == "4":
        name = input("Delete who? ").lower()
        contacts = [c for c in contacts if c["name"].lower() != name]
        save()
        print("Deleted (if they existed).")
    elif choice == "5":
        break

Expense Tracker (CSV + summary) MEDIUM

Build: log expenses by category to a CSV file, then get a per-category spending summary.

import csv, os
from collections import defaultdict

FILE = "expenses.csv"

while True:
    cmd = input("add / summary / quit > ").strip().lower()
    if cmd == "add":
        category = input("Category: ")
        amount = float(input("Amount: $"))
        note = input("Note: ")
        new = not os.path.exists(FILE)
        with open(FILE, "a", newline="") as f:
            w = csv.writer(f)
            if new:
                w.writerow(["category", "amount", "note"])
            w.writerow([category, amount, note])
        print("Logged!")
    elif cmd == "summary":
        totals = defaultdict(float)
        if os.path.exists(FILE):
            for row in csv.DictReader(open(FILE)):
                totals[row["category"]] += float(row["amount"])
        grand = sum(totals.values())
        for cat, amt in sorted(totals.items(), key=lambda x: -x[1]):
            print(f"{cat:<12} ${amt:8.2f}")
        print(f"{'TOTAL':<12} ${grand:8.2f}")
    elif cmd == "quit":
        break

Flashcards Quiz MEDIUM

Build: a study tool: it asks, you answer, it scores you - swap in your own deck.

CARDS = {
    "Capital of France?": "paris",
    "2 squared?": "4",
    "Who created Python?": "guido van rossum",
}

score = 0
for question, answer in CARDS.items():
    guess = input(question + " ").strip().lower()
    if guess == answer:
        print("✅ Correct!")
        score += 1
    else:
        print(f"❌ It was: {answer}")

print(f"\nScore: {score}/{len(CARDS)}")

Vigenère Cipher MEDIUM

Build: the famous 'unbreakable' cipher of the 1500s - each letter shifts by a repeating keyword.

def vigenere(text, key, decrypt=False):
    out = []
    key = key.upper()
    ki = 0
    for c in text.upper():
        if c.isalpha():
            shift = ord(key[ki % len(key)]) - 65
            if decrypt:
                shift = -shift
            out.append(chr((ord(c) - 65 + shift) % 26 + 65))
            ki += 1
        else:
            out.append(c)
    return "".join(out)

message = input("Message: ")
key = input("Keyword: ")

secret = vigenere(message, key)
print(f"Encrypted: {secret}")
print(f"Decrypted: {vigenere(secret, key, decrypt=True)}")

Morse Code Translator MEDIUM

Build: text to beeps and back - one dict powers both directions.

MORSE = {"A": ".-", "B": "-...", "C": "-.-.", "D": "-..", "E": ".", "F": "..-.",
         "G": "--.", "H": "....", "I": "..", "J": ".---", "K": "-.-", "L": ".-..",
         "M": "--", "N": "-.", "O": "---", "P": ".--.", "Q": "--.-", "R": ".-.",
         "S": "...", "T": "-", "U": "..-", "V": "...-", "W": ".--", "X": "-..-",
         "Y": "-.--", "Z": "--..", "0": "-----", "1": ".----", "2": "..---",
         "3": "...--", "4": "....-", "5": ".....", "6": "-....", "7": "--...",
         "8": "---..", "9": "----."}
REVERSE = {code: letter for letter, code in MORSE.items()}

text = input("Text: ").upper()
morse = " ".join("/" if c == " " else MORSE.get(c, "?") for c in text)
print(f"Morse: {morse}")

decoded = "".join(" " if code == "/" else REVERSE.get(code, "?") for code in morse.split(" "))
print(f"Back:  {decoded}")

File Organizer MEDIUM

Build: the classic automation win: sweep a messy folder into Images/, Documents/, Music/… by extension.

import os, shutil

FOLDERS = {".jpg": "Images", ".png": "Images", ".gif": "Images",
           ".pdf": "Documents", ".docx": "Documents", ".txt": "Documents",
           ".mp3": "Music", ".wav": "Music", ".mp4": "Videos", ".zip": "Archives"}

# demo: create a messy folder (point 'target' at your real Downloads to use it for real)
target = "demo_downloads"
os.makedirs(target, exist_ok=True)
for name in ["cat.jpg", "resume.pdf", "song.mp3", "notes.txt", "clip.mp4", "logo.png"]:
    open(os.path.join(target, name), "w").close()

for name in os.listdir(target):
    path = os.path.join(target, name)
    if not os.path.isfile(path):
        continue
    ext = os.path.splitext(name)[1].lower()
    folder = FOLDERS.get(ext, "Other")
    dest = os.path.join(target, folder)
    os.makedirs(dest, exist_ok=True)
    shutil.move(path, os.path.join(dest, name))
    print(f"{name:<12} -> {folder}/")

print("Done - folder organized!")

Bulk File Renamer MEDIUM

Build: rename a whole folder of files to a clean numbered pattern like photo_001.jpg.

import os

# demo: make some messily named files (point 'folder' at a real one to use it)
folder = "vacation_pics"
os.makedirs(folder, exist_ok=True)
for name in ["IMG_20250701.jpg", "DSC0042.jpg", "photo copy (2).jpg", "beach!!.png"]:
    open(os.path.join(folder, name), "w").close()

files = sorted(f for f in os.listdir(folder) if os.path.isfile(os.path.join(folder, f)))
for i, name in enumerate(files, 1):
    ext = os.path.splitext(name)[1].lower()
    new = f"photo_{i:03d}{ext}"
    os.rename(os.path.join(folder, name), os.path.join(folder, new))
    print(f"{name:<22} -> {new}")

print(f"Renamed {len(files)} files.")

HTML Link Extractor MEDIUM

Build: parse real HTML with the standard library's html.parser - the first step to web scraping.

from html.parser import HTMLParser

SAMPLE = '''
<html><body>
  <a href="https://python.org">Python</a>
  <p>Some text <a href="/docs">the docs</a> here.</p>
  <a href="https://pypi.org">PyPI</a>
</body></html>
'''

class LinkExtractor(HTMLParser):
    def __init__(self):
        super().__init__()
        self.links = []
        self._href = None
    def handle_starttag(self, tag, attrs):
        if tag == "a":
            self._href = dict(attrs).get("href")
    def handle_data(self, data):
        if self._href and data.strip():
            self.links.append((data.strip(), self._href))
            self._href = None

parser = LinkExtractor()
parser.feed(SAMPLE)
for text, href in parser.links:
    print(f"{text:<10} -> {href}")

Markdown → HTML Converter MEDIUM

Build: a mini static-site engine: headers, bold, italic, code and lists become real HTML.

import re

SAMPLE = '''# My Blog
This is **bold** and this is *italic* and this is `code`.

## Shopping list
- apples
- bread
- coffee
'''

html = []
in_list = False
for line in SAMPLE.splitlines():
    # inline styles first
    line = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", line)
    line = re.sub(r"\*(.+?)\*", r"<i>\1</i>", line)
    line = re.sub(r"`(.+?)`", r"<code>\1</code>", line)
    if line.startswith("## "):
        html.append(f"<h2>{line[3:]}</h2>")
    elif line.startswith("# "):
        html.append(f"<h1>{line[2:]}</h1>")
    elif line.startswith("- "):
        if not in_list:
            html.append("<ul>")
            in_list = True
        html.append(f"  <li>{line[2:]}</li>")
    else:
        if in_list:
            html.append("</ul>")
            in_list = False
        if line.strip():
            html.append(f"<p>{line}</p>")
if in_list:
    html.append("</ul>")

print("\n".join(html))

Rock Paper Scissors vs a Learning AI MEDIUM

Build: this opponent tracks your habits and counters your favourite move - beat it if you can.

import random
from collections import Counter

BEATS = {"r": "s", "p": "r", "s": "p"}       # what each move defeats
COUNTER = {"r": "p", "p": "s", "s": "r"}     # what defeats each move
NAMES = {"r": "rock", "p": "paper", "s": "scissors"}

history = Counter()
you_score = ai_score = 0

while True:
    move = input("r / p / s (or q to quit): ").strip().lower()
    if move == "q":
        break
    if move not in "rps" or len(move) != 1:
        continue
    # AI predicts you'll repeat your most common move - and counters it
    if history:
        predicted = history.most_common(1)[0][0]
        ai = COUNTER[predicted]
    else:
        ai = random.choice("rps")
    history[move] += 1
    print(f"AI plays {NAMES[ai]}.")
    if ai == move:
        print("Tie!")
    elif BEATS[move] == ai:
        you_score += 1
        print("You win this round!")
    else:
        ai_score += 1
        print("AI wins this round!")
    print(f"Score - you {you_score} : {ai_score} AI")

print("Thanks for playing!")

Any-Base Number Converter MEDIUM

Build: convert between ANY two bases from 2 to 36 - hex, binary, base-7, you name it.

DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

def to_base(n, base):
    if n == 0:
        return "0"
    out = ""
    while n:
        out = DIGITS[n % base] + out
        n //= base
    return out

number = input("Number: ").strip().upper()
from_base = int(input("From base: "))
to_b = int(input("To base: "))

value = int(number, from_base)                 # int() parses any base up to 36
print(f"{number} (base {from_base}) = {to_base(value, to_b)} (base {to_b})")

Blackjack (21) MEDIUM

Build: hit or stand against a dealer that must draw to 17 - with proper ace handling.

import random

def draw():
    return random.choice([2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10, 11])  # 11 = ace

def total(hand):
    t = sum(hand)
    aces = hand.count(11)
    while t > 21 and aces:            # demote aces from 11 to 1 as needed
        t -= 10
        aces -= 1
    return t

you = [draw(), draw()]
dealer = [draw(), draw()]
print(f"Your hand: {you} = {total(you)}")
print(f"Dealer shows: {dealer[0]}")

while total(you) < 21:
    if input("Hit or stand (h/s)? ").strip().lower() != "h":
        break
    you.append(draw())
    print(f"Your hand: {you} = {total(you)}")

if total(you) > 21:
    print("Bust - dealer wins!")
else:
    while total(dealer) < 17:          # dealer must hit until 17
        dealer.append(draw())
    print(f"Dealer: {dealer} = {total(dealer)}")
    if total(dealer) > 21 or total(you) > total(dealer):
        print("You win! 🎉")
    elif total(you) == total(dealer):
        print("Push (tie).")
    else:
        print("Dealer wins!")

Wordle Clone MEDIUM

Build: 6 tries to guess a 5-letter word - 🟩 right spot, 🟨 wrong spot, ⬛ not in the word.

import random

WORDS = ["apple", "brave", "crane", "dream", "eagle", "flame", "grape", "house"]
target = random.choice(WORDS)

for attempt in range(1, 7):
    guess = input(f"Guess {attempt}/6: ").strip().lower()
    if len(guess) != 5:
        print("Need exactly 5 letters.")
        continue
    hint = ""
    for i, c in enumerate(guess):
        if c == target[i]:
            hint += "\U0001f7e9"          # green
        elif c in target:
            hint += "\U0001f7e8"          # yellow
        else:
            hint += "\u2b1b"              # black
    print(hint)
    if guess == target:
        print(f"Solved in {attempt}! \U0001f389")
        break
else:
    print(f"Out of tries - it was {target.upper()}.")

Multi-Unit Converter MEDIUM

Build: one converter for length, weight and temperature - dicts of factors do the math.

LENGTH = {"m": 1, "km": 1000, "cm": 0.01, "mi": 1609.34, "ft": 0.3048, "in": 0.0254}
WEIGHT = {"kg": 1, "g": 0.001, "lb": 0.4536, "oz": 0.02835, "t": 1000}

def convert(value, frm, to, table):
    return value * table[frm] / table[to]

while True:
    cat = input("length / weight / temp (q to quit): ").strip().lower()
    if cat == "q":
        break
    value = float(input("Value: "))
    frm = input("From unit: ").strip().lower()
    to = input("To unit: ").strip().lower()
    if cat == "length":
        print(f"= {convert(value, frm, to, LENGTH):g} {to}")
    elif cat == "weight":
        print(f"= {convert(value, frm, to, WEIGHT):g} {to}")
    elif cat == "temp":
        celsius = {"c": value, "f": (value - 32) * 5 / 9, "k": value - 273.15}[frm]
        result = {"c": celsius, "f": celsius * 9 / 5 + 32, "k": celsius + 273.15}[to]
        print(f"= {result:g}\u00b0{to.upper()}")

Pomodoro Timer MEDIUM

Build: the focus technique: work sprints and short breaks, with a live countdown in the terminal.

import time

work_min = float(input("Work minutes (try 25): "))
break_min = float(input("Break minutes (try 5): "))
sessions = int(input("How many sessions? "))

def countdown(seconds, label):
    for remaining in range(int(seconds), 0, -1):
        m, s = divmod(remaining, 60)
        print(f"\r{label}: {m:02d}:{s:02d} ", end="", flush=True)
        time.sleep(1)
    print(f"\r{label}: done!          ")

for i in range(1, sessions + 1):
    print(f"\n\U0001f345 Session {i}/{sessions} - focus!")
    countdown(work_min * 60, "Work")
    if i < sessions:
        countdown(break_min * 60, "Break")

print("\nAll sessions complete - great work!")

Tic-Tac-Toe AI (unbeatable, minimax) HARD

Build: an AI that never loses - it searches every future game with the minimax algorithm.

import math

board = [" "] * 9
LINES = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)]

def winner(b):
    for a, c, d in LINES:
        if b[a] == b[c] == b[d] != " ":
            return b[a]
    return None

def minimax(b, ai_turn):
    w = winner(b)
    if w == "O": return 1
    if w == "X": return -1
    if " " not in b: return 0
    scores = []
    for i in range(9):
        if b[i] == " ":
            b[i] = "O" if ai_turn else "X"
            scores.append(minimax(b, not ai_turn))
            b[i] = " "
    return max(scores) if ai_turn else min(scores)

def best_move(b):
    best, move = -math.inf, 0
    for i in range(9):
        if b[i] == " ":
            b[i] = "O"
            s = minimax(b, False)
            b[i] = " "
            if s > best:
                best, move = s, i
    return move

while winner(board) is None and " " in board:
    board[int(input("Your move 0-8: "))] = "X"
    if winner(board) is None and " " in board:
        board[best_move(board)] = "O"
    print(board[0:3], board[3:6], board[6:9])
print("Winner:", winner(board) or "Draw")

Bank Account System (OOP) HARD

Build: a class that guards its balance - deposits/withdrawals go through methods (encapsulation).

class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit must be positive")
        self.balance += amount
        return self.balance

    def withdraw(self, amount):
        if amount > self.balance:
            print("Insufficient funds")
            return self.balance
        self.balance -= amount
        return self.balance

    def __str__(self):
        return f"{self.owner}: ${self.balance:.2f}"

acc = Account("Ada", 100)
acc.deposit(50)
acc.withdraw(30)
print(acc)   # Ada: $120.00

Markov Text Generator HARD

Build: learn which words follow which, then babble a new sentence in the same style.

import random

text = "the cat sat on the mat the cat ran to the hat"
words = text.split()

chain = {}
for current, nxt in zip(words, words[1:]):
    chain.setdefault(current, []).append(nxt)

word = random.choice(words)
out = [word]
for _ in range(12):
    if word not in chain:
        break
    word = random.choice(chain[word])
    out.append(word)
print(" ".join(out))

Sudoku Solver (backtracking) HARD

Build: fill a 9×9 grid by trying numbers and backing out of dead ends.

def solve(board):
    spot = find_empty(board)
    if not spot:
        return True
    row, col = spot
    for num in range(1, 10):
        if valid(board, num, row, col):
            board[row][col] = num
            if solve(board):
                return True
            board[row][col] = 0          # backtrack
    return False

def find_empty(board):
    for r in range(9):
        for c in range(9):
            if board[r][c] == 0:
                return r, c
    return None

def valid(board, num, row, col):
    if num in board[row]:
        return False
    if num in [board[r][col] for r in range(9)]:
        return False
    br, bc = 3 * (row // 3), 3 * (col // 3)
    for r in range(br, br + 3):
        for c in range(bc, bc + 3):
            if board[r][c] == num:
                return False
    return True

Minesweeper (reveal logic) HARD

Build: place mines, count neighbours, and flood-fill the empty cells.

import random

SIZE, MINES = 5, 5
mines = set(random.sample(range(SIZE * SIZE), MINES))
revealed = set()

def neighbors(i):
    r, c = divmod(i, SIZE)
    for dr in (-1, 0, 1):
        for dc in (-1, 0, 1):
            nr, nc = r + dr, c + dc
            if 0 <= nr < SIZE and 0 <= nc < SIZE and (dr or dc):
                yield nr * SIZE + nc

def count(i):
    return sum(1 for n in neighbors(i) if n in mines)

def reveal(i):                 # flood-fill empty cells
    if i in revealed or i in mines:
        return
    revealed.add(i)
    if count(i) == 0:
        for n in neighbors(i):
            reveal(n)

reveal(0)
print(f"{len(revealed)} safe cells opened from the corner.")

Text Adventure Game HARD

Build: branching story rooms driven by the player's choices (functions calling functions).

def cave():
    print("You reach a dark cave. Go (left) or (right)?")
    if input("> ").lower() == "left":
        print("A dragon eats you. The end.")
    else:
        print("You find treasure - you win! 🏆")

def start():
    print("You wake in a forest. A path leads (north).")
    if input("> ").lower() == "north":
        cave()
    else:
        print("You wander forever. The end.")

start()

Snake Game (pygame) HARD

Build: the arcade classic with pygame. Needs pip install pygame and a desktop window - it won't run in the browser.

import pygame, random

pygame.init()
W = H = 400
CELL = 20
screen = pygame.display.set_mode((W, H))
clock = pygame.time.Clock()

snake = [(100, 100)]
direction = (CELL, 0)
food = (200, 200)

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_UP:    direction = (0, -CELL)
            if event.key == pygame.K_DOWN:  direction = (0, CELL)
            if event.key == pygame.K_LEFT:  direction = (-CELL, 0)
            if event.key == pygame.K_RIGHT: direction = (CELL, 0)

    head = (snake[0][0] + direction[0], snake[0][1] + direction[1])
    snake.insert(0, head)
    if head == food:
        food = (random.randrange(0, W, CELL), random.randrange(0, H, CELL))
    else:
        snake.pop()

    screen.fill((0, 0, 0))
    for x, y in snake:
        pygame.draw.rect(screen, (0, 230, 100), (x, y, CELL, CELL))
    pygame.draw.rect(screen, (230, 60, 60), (food[0], food[1], CELL, CELL))
    pygame.display.flip()
    clock.tick(10)

pygame.quit()

Photo Manipulation (Pillow) HARD

Build: open an image and edit pixels - grayscale and a colour invert. Needs pip install pillow.

from PIL import Image

img = Image.open("photo.jpg")

# 1) save a grayscale copy
img.convert("L").save("gray.jpg")

# 2) invert the colours, pixel by pixel
pixels = img.load()
for x in range(img.width):
    for y in range(img.height):
        r, g, b = pixels[x, y][:3]
        pixels[x, y] = (255 - r, 255 - g, 255 - b)
img.save("inverted.jpg")
print("Saved gray.jpg and inverted.jpg")

Maze Solver (BFS pathfinding) HARD

Build: find the shortest path through a maze with breadth-first search.

from collections import deque

maze = [
    "S.#..",
    ".##.#",
    "...#.",
    "#.#..",
    "#...E",
]
rows, cols = len(maze), len(maze[0])
start = end = None
for r in range(rows):
    for c in range(cols):
        if maze[r][c] == "S": start = (r, c)
        if maze[r][c] == "E": end = (r, c)

queue = deque([(start, 0)])
seen = {start}
while queue:
    (r, c), dist = queue.popleft()
    if (r, c) == end:
        print(f"Shortest path: {dist} steps")
        break
    for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
        nr, nc = r + dr, c + dc
        if 0 <= nr < rows and 0 <= nc < cols and maze[nr][nc] != "#" and (nr, nc) not in seen:
            seen.add((nr, nc))
            queue.append(((nr, nc), dist + 1))

Pong (pygame) HARD

Build: paddle vs. wall pong with a bouncing ball. Needs pygame + a window - won't run in the browser.

import pygame

pygame.init()
W, H = 600, 400
screen = pygame.display.set_mode((W, H))
clock = pygame.time.Clock()

paddle = pygame.Rect(20, H // 2 - 40, 12, 80)
ball = pygame.Rect(W // 2, H // 2, 14, 14)
bx, by = 4, 4

running = True
while running:
    for e in pygame.event.get():
        if e.type == pygame.QUIT:
            running = False

    keys = pygame.key.get_pressed()
    if keys[pygame.K_UP]:   paddle.y -= 6
    if keys[pygame.K_DOWN]: paddle.y += 6

    ball.x += bx
    ball.y += by
    if ball.top <= 0 or ball.bottom >= H:
        by = -by
    if ball.right >= W:
        bx = -bx
    if ball.colliderect(paddle):
        bx = abs(bx)
    if ball.left <= 0:
        ball.center = (W // 2, H // 2)        # missed -> reset

    screen.fill((0, 0, 0))
    pygame.draw.rect(screen, (255, 255, 255), paddle)
    pygame.draw.ellipse(screen, (255, 255, 255), ball)
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

Space Shooter (pygame) HARD

Build: move, shoot, and destroy falling enemies for points. Needs pygame + a window.

import pygame, random

pygame.init()
W, H = 480, 600
screen = pygame.display.set_mode((W, H))
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 32)

player = pygame.Rect(W // 2 - 20, H - 60, 40, 30)
bullets, enemies, score, spawn = [], [], 0, 0

running = True
while running:
    for e in pygame.event.get():
        if e.type == pygame.QUIT:
            running = False
        elif e.type == pygame.KEYDOWN and e.key == pygame.K_SPACE:
            bullets.append(pygame.Rect(player.centerx - 2, player.top, 4, 12))

    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:  player.x -= 6
    if keys[pygame.K_RIGHT]: player.x += 6

    spawn += 1
    if spawn > 40:
        spawn = 0
        enemies.append(pygame.Rect(random.randint(0, W - 30), -30, 30, 30))

    for b in bullets[:]:
        b.y -= 8
        if b.bottom < 0:
            bullets.remove(b)
    for en in enemies[:]:
        en.y += 3
        if en.colliderect(player):
            running = False
        for b in bullets[:]:
            if en.colliderect(b):
                enemies.remove(en); bullets.remove(b); score += 1
                break

    screen.fill((10, 10, 30))
    pygame.draw.rect(screen, (0, 230, 100), player)
    for b in bullets: pygame.draw.rect(screen, (255, 255, 0), b)
    for en in enemies: pygame.draw.rect(screen, (230, 60, 60), en)
    screen.blit(font.render(f"Score: {score}", True, (255, 255, 255)), (10, 10))
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

Platformer (pygame) HARD

Build: a jumping square with gravity and a ground to land on. Needs pygame + a window.

import pygame

pygame.init()
W, H = 600, 400
screen = pygame.display.set_mode((W, H))
clock = pygame.time.Clock()

player = pygame.Rect(50, 0, 30, 40)
vel_y = 0
GROUND = H - 40
on_ground = False

running = True
while running:
    for e in pygame.event.get():
        if e.type == pygame.QUIT:
            running = False

    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:  player.x -= 5
    if keys[pygame.K_RIGHT]: player.x += 5
    if keys[pygame.K_SPACE] and on_ground:
        vel_y = -15
        on_ground = False

    vel_y += 1                                # gravity
    player.y += vel_y
    if player.bottom >= GROUND:
        player.bottom = GROUND
        vel_y = 0
        on_ground = True

    screen.fill((135, 206, 235))
    pygame.draw.rect(screen, (60, 60, 60), (0, GROUND, W, 40))
    pygame.draw.rect(screen, (220, 50, 50), player)
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

2048 (console) HARD

Build: the addictive sliding-tile game: slide with WASD, merge equal tiles, reach 2048.

import random

def new_tile(grid):
    empty = [(r, c) for r in range(4) for c in range(4) if grid[r][c] == 0]
    if empty:
        r, c = random.choice(empty)
        grid[r][c] = 4 if random.random() < 0.1 else 2

def slide_row(row):
    tiles = [t for t in row if t]                 # squash left
    out, i = [], 0
    while i < len(tiles):
        if i + 1 < len(tiles) and tiles[i] == tiles[i + 1]:
            out.append(tiles[i] * 2)              # merge
            i += 2
        else:
            out.append(tiles[i])
            i += 1
    return out + [0] * (4 - len(out))

def move(grid, key):
    if key in "ad":                                # horizontal
        rows = [row[::-1] if key == "d" else row[:] for row in grid]
        rows = [slide_row(r) for r in rows]
        return [r[::-1] if key == "d" else r for r in rows]
    cols = [[grid[r][c] for r in range(4)] for c in range(4)]
    cols = [col[::-1] if key == "s" else col for col in cols]
    cols = [slide_row(c) for c in cols]
    cols = [col[::-1] if key == "s" else col for col in cols]
    return [[cols[c][r] for c in range(4)] for r in range(4)]

def show(grid):
    print()
    for row in grid:
        print("".join(f"{t or '.':>6}" for t in row))

grid = [[0] * 4 for _ in range(4)]
new_tile(grid); new_tile(grid)
show(grid)

while True:
    key = input("Move (w/a/s/d, q quits): ").strip().lower()
    if key == "q":
        break
    if key not in "wasd" or not key:
        continue
    moved = move(grid, key)
    if moved != grid:
        grid = moved
        new_tile(grid)
    show(grid)
    if any(2048 in row for row in grid):
        print("\U0001f3c6 2048 - you win!")
        break

Connect Four (vs AI) HARD

Build: drop pieces into a 7-wide board; the AI takes winning moves and blocks yours.

import random

ROWS, COLS = 6, 7
board = [[" "] * COLS for _ in range(ROWS)]

def drop(col, piece):
    for r in range(ROWS - 1, -1, -1):
        if board[r][col] == " ":
            board[r][col] = piece
            return r
    return -1

def undo(col):
    for r in range(ROWS):
        if board[r][col] != " ":
            board[r][col] = " "
            return

def wins(piece):
    for r in range(ROWS):
        for c in range(COLS):
            for dr, dc in ((0, 1), (1, 0), (1, 1), (1, -1)):
                cells = [(r + i * dr, c + i * dc) for i in range(4)]
                if all(0 <= rr < ROWS and 0 <= cc < COLS and board[rr][cc] == piece
                       for rr, cc in cells):
                    return True
    return False

def show():
    print("\n 1 2 3 4 5 6 7")
    for row in board:
        print("|" + "|".join(row) + "|")

def ai_move():
    valid = [c for c in range(COLS) if board[0][c] == " "]
    for piece in ("O", "X"):                     # win if possible, else block
        for c in valid:
            drop(c, piece)
            if wins(piece):
                undo(c)
                return c
            undo(c)
    return random.choice(sorted(valid, key=lambda c: abs(c - 3))[:3])

show()
while True:
    raw = input("Your column (1-7, q quits): ").strip().lower()
    if raw == "q":
        break
    if not raw.isdigit() or not 1 <= int(raw) <= 7 or board[0][int(raw) - 1] != " ":
        continue
    drop(int(raw) - 1, "X")
    if wins("X"):
        show(); print("You win! \U0001f389"); break
    if all(board[0][c] != " " for c in range(COLS)):
        show(); print("Draw!"); break
    ai = ai_move()
    drop(ai, "O")
    print(f"AI drops in column {ai + 1}.")
    show()
    if wins("O"):
        print("AI wins!"); break

Battleship (vs computer) HARD

Build: the computer hides 3 ships on a 5×5 grid - call your shots and sink the fleet.

import random

SIZE, SHIPS = 5, 3
ships = set()
while len(ships) < SHIPS:
    ships.add((random.randint(1, SIZE), random.randint(1, SIZE)))

hits, misses = set(), set()
shots = 0

def show():
    print("\n  " + " ".join(str(c) for c in range(1, SIZE + 1)))
    for r in range(1, SIZE + 1):
        row = []
        for c in range(1, SIZE + 1):
            if (r, c) in hits:      row.append("X")
            elif (r, c) in misses:  row.append("o")
            else:                   row.append("~")
        print(f"{r} " + " ".join(row))

print(f"I've hidden {SHIPS} ships in a {SIZE}x{SIZE} sea. Fire with 'row col'.")
show()
while len(hits) < SHIPS:
    raw = input("Shot (row col, q quits): ").strip().lower()
    if raw == "q":
        print(f"Retreating... ships were at {sorted(ships)}")
        break
    try:
        r, c = map(int, raw.split())
    except ValueError:
        continue
    shots += 1
    if (r, c) in ships:
        hits.add((r, c))
        print(f"\U0001f4a5 HIT! ({len(hits)}/{SHIPS})")
    else:
        misses.add((r, c))
        print("Splash... miss.")
    show()
else:
    print(f"\U0001f3c6 Fleet destroyed in {shots} shots!")

Conway's Game of Life HARD

Build: cells live and die by four simple rules - watch a glider walk across your terminal.

SIZE = 15
GLIDER = {(1, 2), (2, 3), (3, 1), (3, 2), (3, 3)}

def neighbors(cell):
    r, c = cell
    return {((r + dr) % SIZE, (c + dc) % SIZE)
            for dr in (-1, 0, 1) for dc in (-1, 0, 1) if (dr, dc) != (0, 0)}

def step(alive):
    counts = {}
    for cell in alive:
        for n in neighbors(cell):
            counts[n] = counts.get(n, 0) + 1
    return {cell for cell, n in counts.items()
            if n == 3 or (n == 2 and cell in alive)}

alive = set(GLIDER)
for gen in range(1, 21):
    print(f"\nGeneration {gen} - {len(alive)} cells")
    for r in range(SIZE):
        print("".join("\u2588" if (r, c) in alive else "\u00b7" for c in range(SIZE)))
    alive = step(alive)

Huffman Compression HARD

Build: the algorithm inside ZIP: build the tree, encode the text, prove the round-trip, measure the savings.

import heapq
from collections import Counter

def build_codes(text):
    heap = [[count, i, char, ""] for i, (char, count) in enumerate(Counter(text).items())]
    heapq.heapify(heap)
    i = len(heap)
    while len(heap) > 1:
        lo = heapq.heappop(heap)
        hi = heapq.heappop(heap)
        merged = [lo[0] + hi[0], i, None, lo, hi]
        i += 1
        heapq.heappush(heap, merged)
    codes = {}
    def walk(node, path):
        if node[2] is not None:              # leaf
            codes[node[2]] = path or "0"
            return
        walk(node[3], path + "0")
        walk(node[4], path + "1")
    walk(heap[0], "")
    return codes

text = "the quick brown fox jumps over the lazy dog the end"
codes = build_codes(text)
encoded = "".join(codes[c] for c in text)

decode_map = {v: k for k, v in codes.items()}
decoded, buffer = "", ""
for bit in encoded:
    buffer += bit
    if buffer in decode_map:
        decoded += decode_map[buffer]
        buffer = ""

assert decoded == text, "round-trip failed!"
plain_bits = len(text) * 8
print(f"Original: {plain_bits} bits   Compressed: {len(encoded)} bits")
print(f"Saved {100 - len(encoded) * 100 // plain_bits}% - and it decodes perfectly.")

Mini JSON Database HARD

Build: build your own tiny MongoDB: insert, query by field, delete - persisted to a JSON file.

import json, os

class MiniDB:
    def __init__(self, path):
        self.path = path
        self.rows = json.load(open(path)) if os.path.exists(path) else []
        self.next_id = max((r["id"] for r in self.rows), default=0) + 1

    def save(self):
        json.dump(self.rows, open(self.path, "w"), indent=2)

    def insert(self, **fields):
        row = {"id": self.next_id, **fields}
        self.rows.append(row)
        self.next_id += 1
        self.save()
        return row

    def find(self, **where):
        return [r for r in self.rows
                if all(r.get(k) == v for k, v in where.items())]

    def delete(self, **where):
        keep = [r for r in self.rows
                if not all(r.get(k) == v for k, v in where.items())]
        removed = len(self.rows) - len(keep)
        self.rows = keep
        self.save()
        return removed

db = MiniDB("people.json")
db.insert(name="Alice", role="engineer", team="ai")
db.insert(name="Bob", role="designer", team="web")
db.insert(name="Cara", role="engineer", team="web")

print("Engineers:", [r["name"] for r in db.find(role="engineer")])
print("Web team: ", [r["name"] for r in db.find(team="web")])
print("Deleted:", db.delete(name="Bob"), "row(s)")
print("Everyone:", [r["name"] for r in db.find()])

Mini Template Engine HARD

Build: how Flask/Django templates work inside: {{ variables }} and {% for %} loops via regex.

import re

def render(template, context):
    # {% for item in items %} ... {% endfor %}
    def do_loop(m):
        var, seq, body = m.group(1), m.group(2), m.group(3)
        out = []
        for item in context[seq]:
            out.append(render(body, {**context, var: item}))
        return "".join(out)
    template = re.sub(
        r"{%\s*for\s+(\w+)\s+in\s+(\w+)\s*%}(.*?){%\s*endfor\s*%}",
        do_loop, template, flags=re.S)
    # {{ variable }} and {{ variable.attribute }}
    def do_var(m):
        parts = m.group(1).split(".")
        value = context[parts[0]]
        for attr in parts[1:]:
            value = value[attr] if isinstance(value, dict) else getattr(value, attr)
        return str(value)
    return re.sub(r"{{\s*([\w.]+)\s*}}", do_var, template)

page = '''<h1>{{ title }}</h1>
<ul>
{% for user in users %}  <li>{{ user.name }} - {{ user.role }}</li>
{% endfor %}</ul>'''

print(render(page, {
    "title": "Team Roster",
    "users": [{"name": "Alice", "role": "engineer"},
              {"name": "Bob", "role": "designer"}],
}))

Neural Network From Scratch HARD

Build: no libraries - just math: a tiny network learns XOR by backpropagation.

import math, random

random.seed(4)
INPUTS = [(0, 0), (0, 1), (1, 0), (1, 1)]
TARGETS = [0, 1, 1, 0]                        # XOR
H = 3                                          # hidden neurons

w1 = [[random.uniform(-1, 1) for _ in range(H)] for _ in range(2)]
b1 = [random.uniform(-1, 1) for _ in range(H)]
w2 = [random.uniform(-1, 1) for _ in range(H)]
b2 = random.uniform(-1, 1)

def sigmoid(x): return 1 / (1 + math.exp(-x))

def forward(x):
    hidden = [sigmoid(x[0] * w1[0][j] + x[1] * w1[1][j] + b1[j]) for j in range(H)]
    output = sigmoid(sum(hidden[j] * w2[j] for j in range(H)) + b2)
    return hidden, output

LR = 0.7
for epoch in range(20000):
    for x, t in zip(INPUTS, TARGETS):
        hidden, out = forward(x)
        d_out = (out - t) * out * (1 - out)                 # output gradient
        for j in range(H):                                   # backprop to hidden
            d_hidden = d_out * w2[j] * hidden[j] * (1 - hidden[j])
            w2[j] -= LR * d_out * hidden[j]
            w1[0][j] -= LR * d_hidden * x[0]
            w1[1][j] -= LR * d_hidden * x[1]
            b1[j] -= LR * d_hidden
        b2 -= LR * d_out

print("Learned XOR:")
correct = 0
for x, t in zip(INPUTS, TARGETS):
    _, out = forward(x)
    correct += round(out) == t
    print(f"  {x} -> {out:.3f}  (want {t})")
print(f"{correct}/4 correct - the network figured it out!")

Genetic Algorithm HARD

Build: evolution in 40 lines: random strings breed, mutate and converge on a target phrase.

import random, string

random.seed(11)
TARGET = "PYTHON IS ALIVE"
ALPHABET = string.ascii_uppercase + " "
POP, MUTATION = 200, 0.05

def fitness(s):
    return sum(a == b for a, b in zip(s, TARGET))

def breed(a, b):
    cut = random.randrange(len(TARGET))
    child = a[:cut] + b[cut:]
    return "".join(random.choice(ALPHABET) if random.random() < MUTATION else c
                   for c in child)

population = ["".join(random.choice(ALPHABET) for _ in TARGET) for _ in range(POP)]

for gen in range(1, 1001):
    population.sort(key=fitness, reverse=True)
    best = population[0]
    if gen % 25 == 0 or best == TARGET:
        print(f"gen {gen:4d}: {best}  ({fitness(best)}/{len(TARGET)})")
    if best == TARGET:
        print(f"\U0001f9ec Evolved the target in {gen} generations!")
        break
    parents = population[:POP // 5]            # top 20% survive and breed
    population = [breed(random.choice(parents), random.choice(parents))
                  for _ in range(POP)]

A* Pathfinding HARD

Build: the algorithm behind game NPCs and GPS routing - watch it thread a maze optimally.

import heapq

MAZE = [
    "S..#......",
    ".#.#.####.",
    ".#.#....#.",
    ".#.####.#.",
    ".#......#.",
    ".########.",
    "..........",
    ".####.###.",
    ".....#...G",
]
grid = [list(row) for row in MAZE]
ROWS, COLS = len(grid), len(grid[0])
start = next((r, c) for r in range(ROWS) for c in range(COLS) if grid[r][c] == "S")
goal = next((r, c) for r in range(ROWS) for c in range(COLS) if grid[r][c] == "G")

def h(cell):                                   # manhattan distance heuristic
    return abs(cell[0] - goal[0]) + abs(cell[1] - goal[1])

open_set = [(h(start), 0, start, None)]
came_from, cost = {}, {start: 0}
while open_set:
    _, g, cell, parent = heapq.heappop(open_set)
    if cell in came_from:
        continue
    came_from[cell] = parent
    if cell == goal:
        break
    r, c = cell
    for nr, nc in ((r+1, c), (r-1, c), (r, c+1), (r, c-1)):
        if 0 <= nr < ROWS and 0 <= nc < COLS and grid[nr][nc] != "#":
            if (nr, nc) not in cost or g + 1 < cost[(nr, nc)]:
                cost[(nr, nc)] = g + 1
                heapq.heappush(open_set, (g + 1 + h((nr, nc)), g + 1, (nr, nc), cell))

cell = goal
path_len = 0
while came_from.get(cell) is not None:
    cell = came_from[cell]
    if grid[cell[0]][cell[1]] == ".":
        grid[cell[0]][cell[1]] = "*"
        path_len += 1

for row in grid:
    print("".join(row))
print(f"Shortest path: {path_len + 1} steps")

Mini Language Interpreter HARD

Build: write your own programming language: a tokenizer, a recursive-descent parser and variables.

import re

TOKEN = re.compile(r"\s*(\d+\.?\d*|[A-Za-z_]\w*|[-+*/()=])")

def tokenize(line):
    pos, out = 0, []
    while pos < len(line):
        m = TOKEN.match(line, pos)
        if not m:
            raise SyntaxError(f"bad character at: {line[pos:]}")
        out.append(m.group(1))
        pos = m.end()
    return out

class Interpreter:
    def __init__(self):
        self.vars = {}

    def run(self, line):
        self.tokens = tokenize(line)
        self.i = 0
        if len(self.tokens) >= 2 and self.tokens[1] == "=":     # assignment
            name = self.tokens[0]
            self.i = 2
            self.vars[name] = self.expr()
            return None
        return self.expr()

    def peek(self):
        return self.tokens[self.i] if self.i < len(self.tokens) else None

    def expr(self):                    # + and -
        value = self.term()
        while self.peek() in ("+", "-"):
            op = self.tokens[self.i]; self.i += 1
            value = value + self.term() if op == "+" else value - self.term()
        return value

    def term(self):                    # * and / bind tighter
        value = self.factor()
        while self.peek() in ("*", "/"):
            op = self.tokens[self.i]; self.i += 1
            value = value * self.factor() if op == "*" else value / self.factor()
        return value

    def factor(self):                  # numbers, names, parentheses
        tok = self.tokens[self.i]; self.i += 1
        if tok == "(":
            value = self.expr()
            self.i += 1               # consume ")"
            return value
        if tok.replace(".", "").isdigit():
            return float(tok) if "." in tok else int(tok)
        return self.vars[tok]

interp = Interpreter()
print("Mini-language REPL - try: x = 5   then   x * 3 + 2   (quit to exit)")
while True:
    line = input(">>> ").strip()
    if line == "quit":
        break
    if not line:
        continue
    result = interp.run(line)
    if result is not None:
        print(result)

Sudoku Generator HARD

Build: not just solving - generating: build a full valid board, then carve out clues while keeping the solution unique.

import random

random.seed(3)

def valid(board, r, c, v):
    if any(board[r][i] == v or board[i][c] == v for i in range(9)):
        return False
    br, bc = r - r % 3, c - c % 3
    return all(board[br + i][bc + j] != v for i in range(3) for j in range(3))

def fill(board, pos=0):
    if pos == 81:
        return True
    r, c = divmod(pos, 9)
    if board[r][c]:
        return fill(board, pos + 1)
    for v in random.sample(range(1, 10), 9):
        if valid(board, r, c, v):
            board[r][c] = v
            if fill(board, pos + 1):
                return True
            board[r][c] = 0
    return False

def count_solutions(board, pos=0, cap=2):
    if pos == 81:
        return 1
    r, c = divmod(pos, 9)
    if board[r][c]:
        return count_solutions(board, pos + 1, cap)
    total = 0
    for v in range(1, 10):
        if valid(board, r, c, v):
            board[r][c] = v
            total += count_solutions(board, pos + 1, cap)
            board[r][c] = 0
            if total >= cap:
                break
    return total

board = [[0] * 9 for _ in range(9)]
fill(board)
solution = [row[:] for row in board]

removed = 0
for r, c in random.sample([(r, c) for r in range(9) for c in range(9)], 81):
    if removed >= 40:
        break
    saved, board[r][c] = board[r][c], 0
    if count_solutions([row[:] for row in board]) != 1:   # must stay unique
        board[r][c] = saved
    else:
        removed += 1

print(f"Puzzle ({81 - removed} clues, unique solution):")
for r in range(9):
    print(" ".join(str(v) if v else "." for v in board[r]))

Boggle Solver HARD

Build: find every dictionary word hidden in a 4×4 letter grid - DFS with prefix pruning.

GRID = ["tape", "ears", "note", "send"]
WORDS = ["tap", "tape", "ear", "ears", "earn", "note", "notes", "send", "sent",
         "art", "rat", "rate", "eat", "tea", "ten", "net", "nets", "sea", "seat",
         "toe", "ton", "tone", "ants", "pear", "pare", "aper"]

word_set = set(WORDS)
prefixes = {w[:i] for w in WORDS for i in range(1, len(w) + 1)}

found = set()
def dfs(r, c, path, visited):
    path += GRID[r][c]
    if path not in prefixes:
        return                              # prune dead branches early
    if path in word_set and len(path) >= 3:
        found.add(path)
    for dr in (-1, 0, 1):
        for dc in (-1, 0, 1):
            nr, nc = r + dr, c + dc
            if (0 <= nr < 4 and 0 <= nc < 4 and (nr, nc) not in visited
                    and (dr, dc) != (0, 0)):
                dfs(nr, nc, path, visited | {(nr, nc)})

for r in range(4):
    for c in range(4):
        dfs(r, c, "", {(r, c)})

print("Board:")
for row in GRID:
    print("  " + " ".join(row.upper()))
print(f"\nFound {len(found)} words: {', '.join(sorted(found))}")

Virtual File System HARD

Build: build a working shell - mkdir, cd, ls, touch, write, cat, pwd - over an in-memory tree.

root = {}                                     # dirs are dicts, files are strings
cwd, path = root, []

def resolve():
    return "/" + "/".join(path)

print("Mini shell: mkdir cd ls touch write cat pwd exit")
while True:
    parts = input(f"{resolve()} $ ").strip().split(maxsplit=2)
    if not parts:
        continue
    cmd = parts[0]
    if cmd == "exit":
        break
    elif cmd == "mkdir" and len(parts) > 1:
        cwd[parts[1]] = {}
    elif cmd == "touch" and len(parts) > 1:
        cwd[parts[1]] = ""
    elif cmd == "write" and len(parts) > 2:
        cwd[parts[1]] = parts[2]
    elif cmd == "cat" and len(parts) > 1:
        item = cwd.get(parts[1])
        print(item if isinstance(item, str) else "not a file")
    elif cmd == "ls":
        for name, item in sorted(cwd.items()):
            print(name + ("/" if isinstance(item, dict) else ""))
    elif cmd == "pwd":
        print(resolve())
    elif cmd == "cd" and len(parts) > 1:
        if parts[1] == "..":
            if path:
                path.pop()
                cwd = root
                for p in path:
                    cwd = cwd[p]
        elif isinstance(cwd.get(parts[1]), dict):
            cwd = cwd[parts[1]]
            path.append(parts[1])
        else:
            print("no such directory")
    else:
        print("bad command")

File Encryption Tool HARD

Build: encrypt any file with an XOR keystream + base64, decrypt it back, and prove the bytes match.

import base64, hashlib

def keystream(key, length):
    out = b""
    counter = 0
    while len(out) < length:                   # stretch the key with sha256 blocks
        out += hashlib.sha256(key + counter.to_bytes(4, "big")).digest()
        counter += 1
    return out[:length]

def encrypt(data, password):
    ks = keystream(password.encode(), len(data))
    return base64.b64encode(bytes(a ^ b for a, b in zip(data, ks)))

def decrypt(blob, password):
    data = base64.b64decode(blob)
    ks = keystream(password.encode(), len(data))
    return bytes(a ^ b for a, b in zip(data, ks))

# demo on a real file
with open("secret.txt", "w") as f:
    f.write("The launch code is 0451. Tell no one.")

plain = open("secret.txt", "rb").read()
blob = encrypt(plain, "hunter2")
open("secret.txt.enc", "wb").write(blob)
print(f"Encrypted -> secret.txt.enc ({len(blob)} bytes of base64)")
print(f"Ciphertext preview: {blob[:40].decode()}...")

restored = decrypt(open("secret.txt.enc", "rb").read(), "hunter2")
assert restored == plain, "decryption failed!"
print("Decrypted matches the original - round-trip verified. \U0001f512")

Stock Portfolio Tracker (FIFO) HARD

Build: track buys and sells the way accountants do: FIFO lots, realized gains, average cost.

from collections import deque

TRADES = [
    ("buy",  "AAPL", 10, 150.00),
    ("buy",  "AAPL", 10, 170.00),
    ("sell", "AAPL", 15, 200.00),
    ("buy",  "MSFT",  5, 300.00),
    ("sell", "MSFT",  2, 350.00),
]

lots = {}                                     # symbol -> deque of [shares, price]
realized = {}

for action, symbol, shares, price in TRADES:
    if action == "buy":
        lots.setdefault(symbol, deque()).append([shares, price])
    else:                                     # sell oldest shares first (FIFO)
        remaining = shares
        gain = 0.0
        queue = lots[symbol]
        while remaining > 0:
            lot = queue[0]
            take = min(lot[0], remaining)
            gain += take * (price - lot[1])
            lot[0] -= take
            remaining -= take
            if lot[0] == 0:
                queue.popleft()
        realized[symbol] = realized.get(symbol, 0.0) + gain

print(f"{'symbol':<8}{'shares':>8}{'avg cost':>10}{'realized':>12}")
for symbol in sorted(set(list(lots) + list(realized))):
    queue = lots.get(symbol, deque())
    held = sum(s for s, _ in queue)
    avg = sum(s * p for s, p in queue) / held if held else 0
    print(f"{symbol:<8}{held:>8}{avg:>10.2f}{realized.get(symbol, 0):>12.2f}")

Text Editor (tkinter GUI) HARD

Build: a real windowed editor with New/Open/Save, a menu bar and a live word count.

import tkinter as tk
from tkinter import filedialog, messagebox

class Editor:
    def __init__(self, root):
        self.root = root
        self.file = None
        root.title("PyEdit - untitled")
        self.text = tk.Text(root, wrap="word", undo=True, font=("Consolas", 12))
        self.text.pack(fill="both", expand=True)
        self.status = tk.Label(root, text="0 words", anchor="e")
        self.status.pack(fill="x")
        self.text.bind("<KeyRelease>", self.update_count)

        menu = tk.Menu(root)
        filemenu = tk.Menu(menu, tearoff=0)
        filemenu.add_command(label="New", command=self.new, accelerator="Ctrl+N")
        filemenu.add_command(label="Open...", command=self.open, accelerator="Ctrl+O")
        filemenu.add_command(label="Save", command=self.save, accelerator="Ctrl+S")
        filemenu.add_separator()
        filemenu.add_command(label="Quit", command=root.quit)
        menu.add_cascade(label="File", menu=filemenu)
        root.config(menu=menu)
        root.bind("<Control-n>", lambda e: self.new())
        root.bind("<Control-o>", lambda e: self.open())
        root.bind("<Control-s>", lambda e: self.save())

    def update_count(self, event=None):
        words = len(self.text.get("1.0", "end").split())
        self.status.config(text=f"{words} words")

    def new(self):
        self.text.delete("1.0", "end")
        self.file = None
        self.root.title("PyEdit - untitled")

    def open(self):
        path = filedialog.askopenfilename(filetypes=[("Text", "*.txt"), ("All", "*.*")])
        if path:
            self.text.delete("1.0", "end")
            self.text.insert("1.0", open(path, encoding="utf-8").read())
            self.file = path
            self.root.title(f"PyEdit - {path}")
            self.update_count()

    def save(self):
        if not self.file:
            self.file = filedialog.asksaveasfilename(defaultextension=".txt")
        if self.file:
            open(self.file, "w", encoding="utf-8").write(self.text.get("1.0", "end-1c"))
            self.root.title(f"PyEdit - {self.file}")
            messagebox.showinfo("Saved", "File saved!")

root = tk.Tk()
root.geometry("700x480")
Editor(root)
root.mainloop()

HTTP Server From Scratch HARD

Build: no frameworks, no http.server - raw sockets: parse the request line, speak the protocol, serve a page.

import socket, threading, urllib.request

HOST, PORT = "127.0.0.1", 8901

PAGE = "<html><body><h1>Hello from raw sockets!</h1><p>You built a web server.</p></body></html>"

def serve_once():
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((HOST, PORT))
    server.listen(1)
    conn, addr = server.accept()
    request = conn.recv(4096).decode()
    method, target = request.split("\r\n")[0].split()[:2]
    print(f"[server] {addr[0]} asked: {method} {target}")
    body = PAGE.encode()
    response = (b"HTTP/1.1 200 OK\r\n"
                b"Content-Type: text/html\r\n"
                b"Content-Length: " + str(len(body)).encode() + b"\r\n"
                b"Connection: close\r\n\r\n" + body)
    conn.sendall(response)
    conn.close()
    server.close()

thread = threading.Thread(target=serve_once)
thread.start()

# now be our own first visitor
with urllib.request.urlopen(f"http://{HOST}:{PORT}/hello") as reply:
    print(f"[client] status: {reply.status}")
    print(f"[client] body:   {reply.read().decode()[:60]}...")
thread.join()
print("Server handled a real HTTP request - protocol implemented by hand!")
🎉 That's the entire Python course on one page - from print() to OOP, modules, automation and machine learning. You're ready to build real things! Bookmark this page and come back whenever you need a quick reminder.