Seventeen languages, taught from scratch — plus the actual working method behind the code: the loop to run, the principles to hold, and the habits to refuse to break. No secret syntax, no trick. Steal what's useful.
The 14 ideas every language shares — variables, loops, functions, arrays. Learn them once, here.
open basics → 02Seventeen languages, each with its own page of lessons — from "hello world" up to the advanced stuff.
browse languages → 03Syntax isn't the hard part — judgment is. This is the loop, the principles, and the habits behind good code.
read the method →Every task — a one-line fix or a new module — goes through the same six stages. It's a loop, not a line: verifying often sends you back to understanding. Click a stage to open it.
The loop is the choreography; these are the convictions that decide what "good" means at each step. Open one to see it in code.
Pick a small problem and step through it the way it should actually go — clarifying before coding, naming the edge cases, then writing the simplest thing that survives them.
None of these is clever. They're the boring decisions to make the same way every time, because consistency is what makes code skimmable. Each is a real before / after.
Habits are mostly defined by what you don't do. These are the moves to catch yourself reaching for and put back down.
Read this snippet. It runs. It even passes a happy-path test. Decide what you'd change before revealing how to review it.
// returns the discounted price for a cart function calc(items, c) { let t = 0 for (let i = 0; i < items.length; i++) { t = t + items[i].p * items[i].q } if (c == "SAVE10") { t = t * 0.9 } try { logAnalytics(t) } catch (e) {} return t }
calc, t, c, p, q force the reader to decode. Rename to cartTotal, total, couponCode, price, quantity — now no comment is needed.== instead of ===. Loose equality invites coercion bugs. Default to strict equality unless there's a specific reason not to."SAVE10" and 0.9 belong in a lookup the rest of the system can share, not buried in an if.catch swallows failures. If analytics breaks, no one ever finds out. Catch only what you can act on, and at minimum log it — silent failure is the expensive kind.quantity, a negative price? Decide the contract on purpose rather than letting NaN leak downstream.This is the pass to run before saying a piece of code is finished. Tick them off — it's the same gate every piece of code has to clear.
Variables, loops, functions, arrays — these are the grammar of programming. Learn them once and every language becomes a matter of new spelling, not new logic.
Coding is writing precise, step-by-step instructions that a computer follows exactly. A computer has no judgement of its own — it does precisely what you tell it, which means every other concept on this page exists to help you tell it clearly.
// a tiny program: three instructions, run in order
let temperature = 72 // 1. store a value
let isWarm = temperature > 65 // 2. work out a fact from it
print(isWarm) // 3. show the result → trueBefore you can write or run any code, you need a place to write it. An IDE (Integrated Development Environment) or code editor gives you syntax highlighting, error checking, and a way to run your code — all in one place.
1. Go to code.visualstudio.com
2. Click "Download" (it detects your OS automatically)
3. Run the installer, accept the defaults
4. Open VS Code → install a language extension
(e.g. "Python" or "ESLint") from the Extensions panelPyCharm — Python-focused, great built-in debugger
WebStorm — deep JavaScript/TypeScript tooling
Xcode — required for iOS/macOS apps (Mac only)
Android Studio — required for Android apps
Replit / CodeSandbox — zero-install, runs in the browserA comment is a note left in the code for humans. The computer skips it entirely — it exists purely to explain why the code does something, since the code itself already shows what it does.
//, #, --...) but the idea is identical: anything after the marker is invisible to the computer.
// JavaScript: everything after // is ignored
let price = 19.99; // price in US dollars
# Python: everything after # is ignored
price = 19.99 # price in US dollarsA variable is a named box that holds a value. Give it a name once, and you can read or change what's inside that box anywhere later in the program, instead of retyping the value every time.
let score = 0 // declare 'score', starting at 0
score = score + 10 // change what's inside the box: now 10
print(score) // read the box → 10Every value in a program belongs to a type — a category that says what kind of data it is and what you're allowed to do with it. Mixing up types is one of the most common sources of bugs.
let age = 12 // number
let name = "Aria" // string (text)
let isAdmin = false // boolean (true / false)
let nothing = null // 'no value' — its own special typeA string is text — any sequence of characters wrapped in quotes. Strings have their own toolbox of operations: joining, searching, slicing out a piece, and changing case.
let first = "Aria"
let full = first + " Ren" // joining: "Aria Ren"
print(full.length) // 8 characters
print(full.toUpperCase()) // "ARIA REN"
print(full.includes("Ren")) // true — search inside texttext[0].Operators are symbols that act on values: arithmetic (+ - * /), comparison (== > <), and logical (&& || !). They're the verbs that turn values into new values or true/false answers.
let total = 4 + 3 * 2 // arithmetic → 10 (multiply first)
let isAdult = age >= 18 // comparison → true or false
let canEnter = isAdult && hasTicket // logical: both must be true= assigns a value; a double == (or triple ===) compares two values. Mixing them up is a classic bug.Conditionals let a program choose what to do based on a condition. if runs a block only when something is true; else if checks more options; else is the fallback when nothing else matched.
else catches everything else.
if (hp > 50) {
print("healthy")
} else if (hp > 0) {
print("wounded") // this one runs, if hp is e.g. 30
} else {
print("defeated")
}&& (and) or || (or).A loop repeats a block of code so you don't have to copy-paste it. for loops repeat a known number of times (or once per item in a list); while loops repeat until a condition becomes false.
for loop is great when you know how many times (or what you're looping over). A while loop is for "keep going until something changes."
for (let i = 1; i <= 5; i++) {
print(i) // 1, 2, 3, 4, 5
}
let n = 1
while (n < 100) {
n = n * 2 // keep doubling until n reaches 100+
}A function is a named, reusable block of code that performs a task. You give it a name, optionally hand it some inputs, and it can hand back a result. Functions keep programs organised instead of one giant block of code.
return sends a result back to wherever the function was called.
function add(a, b) {
return a + b // hand the result back
}
print(add(2, 3)) // call it → 5
print(add(10, 20)) // reuse it → 30return (it just does something, like printing), that's completely fine too.An array (or list) holds many values in order, under one name. You can read, add, or remove items by their position — far easier than juggling separate variables for every item.
let fruits = ["apple", "banana", "cherry"]
print(fruits[0]) // apple — first item
print(fruits.length) // 3 — how many items
fruits.push("date") // add to the end → 4 items nowfor (let item of fruits) walks every item without you tracking the position yourself.An object bundles related data (and sometimes actions) together under one name, with each piece labelled by a key. Where an array is "many values in order," an object is "several named values describing one thing."
let player = {
name: "Aria",
level: 7,
score: 100
}
print(player.name) // Aria — read by key
player.level = 8 // change one fieldInput is how data enters your program — user typing, a file, a network request. Output is how a program shows or sends results back — printing to a screen, writing a file, returning data from an API.
let name = input("What's your name? ") // INPUT: wait for typing
print("Hello, " + name + "!") // OUTPUT: show a resultDebugging is finding and fixing mistakes in code. Every programmer does this constantly — the skill isn't avoiding bugs entirely, it's noticing them quickly and tracking down the cause methodically.
let total = price * quantity
print("price:", price) // check each input...
print("quantity:", quantity) // ...to see which one is wrong
print("total:", total) // then check the resultBefore you dive into a specific language, it helps to understand the big ideas that most languages share. OOP, functional thinking, types, memory, errors — these get their own page, so you can read them whenever you like.
Most popular languages let you organise your code around objects — little bundles of related data and actions. This style is called Object-Oriented Programming, or OOP. You'll see it everywhere, so it's worth understanding the four core ideas.
// the blueprint:
class Dog {
name = "unknown"
breed = "unknown"
speak() { return this.name + " says Woof!" }
}
// two objects built from it:
const rex = new Dog()
rex.name = "Rex"
const mia = new Dog()
mia.name = "Mia"
rex.speak() // "Rex says Woof!"
mia.speak() // "Mia says Woof!"
Every time you write new Dog() you build a fresh, independent object. Changing rex.name has no effect on mia.class BankAccount {
#balance = 0 // private — nobody outside can touch this directly
deposit(amount) {
if (amount > 0) this.#balance += amount // guarded by a check
}
getBalance() {
return this.#balance // safe read-only access
}
}
const acc = new BankAccount()
acc.deposit(100)
console.log(acc.getBalance()) // 100
// acc.#balance = 9999 // error! private field
By hiding #balance, the object can guarantee it's never set to something nonsensical. The outside world uses deposit() and getBalance() — safe doorways in and out.class Animal {
constructor(name) { this.name = name }
speak() { return this.name + " makes a sound" }
}
class Dog extends Animal { // Dog inherits from Animal
speak() { // replace Animal's speak with our own
return this.name + " barks"
}
fetch(item) { // new method only Dog has
return this.name + " fetches the " + item
}
}
const d = new Dog("Rex")
d.speak() // "Rex barks" (Dog's version)
d.fetch("ball") // "Rex fetches the ball"
Dog didn't have to re-write the constructor — it inherited it. It only provided what was different.class Shape {
area() { return 0 }
}
class Circle extends Shape {
constructor(r) { super(); this.r = r }
area() { return Math.PI * this.r * this.r }
}
class Rect extends Shape {
constructor(w, h) { super(); this.w = w; this.h = h }
area() { return this.w * this.h }
}
// one function works with ANY Shape:
function printArea(shape) {
console.log("area:", shape.area().toFixed(2))
}
printArea(new Circle(5)) // area: 78.54
printArea(new Rect(4, 6)) // area: 24.00
printArea doesn't care whether it receives a Circle or a Rect — it just calls .area() and each object does the right thing.class EmailSender {
// the outside world just calls send() — simple!
send(to, subject, body) {
this.#connect() // complex internal steps
this.#authenticate() // all hidden away
this.#transmit(to, subject, body)
this.#disconnect()
}
#connect() { /* ... open socket, TLS handshake ... */ }
#authenticate() { /* ... SMTP login ... */ }
#transmit() { /* ... format headers, send bytes ... */ }
#disconnect() { /* ... close connection ... */ }
}
const mailer = new EmailSender()
mailer.send("aria@example.com", "Hi", "Hello!") // dead simple to useFunctional programming (FP) is another way of thinking about code. Instead of objects, FP focuses on functions — especially small, focused, pure functions that you chain together. You already use it when you write .map() or .filter().
// pure: same input always gives same output, no side effects
function add(a, b) { return a + b }
add(2, 3) // always 5 — predictable and safe to test
// impure: depends on or changes something outside itself
let total = 0
function addToTotal(n) { total += n } // changes 'total' — side effect!
Pure functions are easy to test (just check the output), easy to reason about, and safe to run many times or in parallel.// mutable approach — changes the original:
const scores = [10, 20, 30]
scores.push(40) // original array mutated
// immutable approach — create a new array instead:
const scores2 = [10, 20, 30]
const newScores = [...scores2, 40] // original unchanged
console.log(scores2) // [10, 20, 30] — still safe
console.log(newScores) // [10, 20, 30, 40]// store a function in a variable:
const double = n => n * 2
// pass a function as an argument:
const numbers = [1, 2, 3, 4, 5]
const doubled = numbers.map(double) // pass 'double' as the rule
console.log(doubled) // [2, 4, 6, 8, 10]
// return a function from a function:
function multiplier(factor) {
return n => n * factor // returns a new function each time
}
const triple = multiplier(3)
console.log(triple(5)) // 15.map(), .filter(), or a callback, you've already done this.const players = [
{ name: "Aria", score: 80 },
{ name: "Bryn", score: 45 },
{ name: "Cy", score: 92 },
]
// map: transform every item
const names = players.map(p => p.name)
// ["Aria", "Bryn", "Cy"]
// filter: keep only items that pass a test
const passing = players.filter(p => p.score >= 50)
// [{ name: "Aria", score: 80 }, { name: "Cy", score: 92 }]
// reduce: combine all items into one value
const total = players.reduce((sum, p) => sum + p.score, 0)
// 217 (80 + 45 + 92)
// chain them — highest scorer's name:
const top = players
.filter(p => p.score >= 80)
.sort((a, b) => b.score - a.score)
.map(p => p.name)
// ["Cy", "Aria"]map → transform. filter → keep some. reduce → collapse to one.Every value in a program has a type — it's a number, or text, or true/false, or something else. A type system is the set of rules about what types exist and how they can mix. Some languages are strict about types; others are looser.
// JavaScript — loosely typed:
let x = 42 // a number
let y = "hello" // a string
console.log(x + 1) // 43 makes sense
console.log(y + "!") // "hello!" makes sense
console.log(x + y) // "42hello" JS converts x to text — surprising!// TypeScript — static (types checked before running):
let score: number = 0
// score = "high" // error caught immediately — can't be string
// Python — dynamic (types checked at runtime):
score = 0
score = "high" // fine — the type just changed
print(score + 1) // crash! only discovered when this line runs
Popular static languages: TypeScript, Java, C#, Rust, Go, Kotlin, Swift.
Popular dynamic languages: Python, JavaScript, Ruby, PHP.// JavaScript — weakly typed:
console.log(1 + "2") // "12" (number converted to string silently)
console.log(0 == "0") // true (0 and "0" are "equal")
console.log(0 === "0") // false (strict equality — different types)
// Python — strongly typed:
# print(1 + "2") # TypeError! Python refuses to mix them
print(1 + int("2")) # 3 — you must convert explicitly
JavaScript's === is the strict equality operator — it checks type AND value, unlike == which can silently convert. Always use === in JavaScript.// TypeScript — you CAN label, but you don't have to:
let name: string = "Aria" // explicit label
let level = 7 // TypeScript infers: number
// Rust — always infers when it can:
let score = 100; // Rust infers: i32
let name = "Aria"; // Rust infers: &str
// Go:
name := "Aria" // Go infers: string
level := 7 // Go infers: int
Even when inference is available, labels are sometimes added for clarity — especially on function parameters and return types.Every value your program creates has to live somewhere in the computer's memory. Understanding the basics of how memory is managed helps you understand why languages behave the way they do — and why some are faster than others.
function main() {
let x = 5 // stack: small number, gone when function ends
let arr = [1,2,3] // heap: array lives here, longer-lived
}
// when main() returns, x is instantly freed from the stack
// the array on the heap is freed by the garbage collector// Python — garbage collector handles everything:
def make_player():
player = { "name": "Aria" } # memory allocated
return player # returned — still needed
p = make_player()
p = None # old object no longer referenced
# garbage collector will free the old object automatically
The GC occasionally has to pause briefly to clean up — in most programs this is invisible, but in games or real-time systems it can cause tiny stutters.malloc (or new) allocates; free (or delete) releases. Forget to free, and you have a memory leak. Free it twice, and the program crashes. This is powerful but error-prone.
// C++ (old style):
int* arr = new int[10]; // allocate 10 ints on the heap
arr[0] = 42;
delete[] arr; // must manually free — forget this and it leaks!
// C++ modern style (smart pointers do it automatically):
auto arr = std::make_unique<int[]>(10);
arr[0] = 42;
// freed automatically when 'arr' goes out of scopefn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2 — s1 is gone
// println!("{}", s1); // compile error! s1 no longer owns the string
println!("{}", s2); // fine
let n = 5;
let m = n; // n is COPIED (numbers are cheap to copy)
println!("{} {}", n, m); // both valid: 5 5
} // s2 is freed here automatically — no GC needed
This means Rust is as fast as C++ but without the memory bugs.Every programmer gets errors. They're not a sign of failure — they're information. The faster you can read and understand an error, the faster you can fix it. There are a few distinct kinds of error, and a set of tools for tracking them down.
// SyntaxError — missing closing bracket:
if (x > 0 { // ← missing )
console.log(x)
}
Runtime errors — the code looks fine but something goes wrong when it runs. Like dividing by zero or calling a method on null.
// TypeError at runtime — null has no .length:
const name = null
console.log(name.length) // crashes here when it runs
Logic errors — the code runs fine and produces output, but the output is wrong. These are the hardest to find.
// Logic error — wrong operator:
function isOdd(n) { return n % 2 == 0 } // should be != 0
isOdd(3) // returns false — runs fine, but the answer is wrong// Example Python error:
Traceback (most recent call last):
File "game.py", line 14, in start_game
result = score / rounds
ZeroDivisionError: division by zero
What it tells you:
• The file: game.py
• The line: 14
• The function: start_game
• The exact line: result = score / rounds
• The type of error: ZeroDivisionError
• The reason: division by zero
Go to line 14. Ask yourself: when could rounds be zero?print() or console.log() at different points to see what values actually are.
function calculateTotal(items) {
console.log("items:", items) // check what came in
const prices = items.map(i => i.price)
console.log("prices:", prices) // check the mapping
return prices.reduce((sum, p) => sum + p, 0)
}
Rubber duck debugging — explain your code out loud to someone (or an imaginary duck). The act of explaining forces you to think through every step, and you often find the bug yourself mid-sentence.
The debugger — a tool that lets you pause a running program at any line and inspect every variable. More powerful than print, but print is often enough.
Binary search — if something is wrong somewhere in a 100-line function, add a print halfway. If the values are correct there, the bug is in the second half. Keep halving until you find it.// JavaScript:
try {
const data = JSON.parse(userInput) // might throw SyntaxError
processData(data)
} catch (err) {
console.log("Invalid input:", err.message) // handle it here
} finally {
cleanup() // always runs, error or not
}
// Python:
try:
result = 10 / user_number
except ZeroDivisionError:
print("Please enter a number that isn't zero")
except ValueError:
print("That doesn't look like a number")
The rule: only catch errors you can meaningfully handle. Don't silently swallow every error — that hides real bugs.Beyond OOP and FP, programmers have developed a set of repeating solutions to common problems — called design patterns. You don't need to memorise them all, but recognising a few gives you a language for talking about code and a toolkit for common situations.
// bad — one function doing three things:
function processOrder(order) {
const total = order.items.reduce((s, i) => s + i.price, 0)
console.log("Order total: " + total)
database.save({ order, total })
emailService.send(order.email, "Your total: " + total)
}
// better — each function does one thing:
function calculateTotal(order) { return order.items.reduce(...) }
function logTotal(total) { console.log("Total:", total) }
function saveOrder(order, total){ database.save({ order, total }) }
function emailConfirmation(email, total) { emailService.send(...) }
The split version is easier to test, easier to change, and easier to understand.// wet (repeated logic):
const tax1 = price1 * 0.2
const tax2 = price2 * 0.2
const tax3 = price3 * 0.2
// dry (one place to change if the tax rate ever changes):
function tax(price) { return price * 0.2 }
const tax1 = tax(price1)
const tax2 = tax(price2)
const tax3 = tax(price3)// a simple event emitter:
class EventEmitter {
#listeners = {}
on(event, fn) {
if (!this.#listeners[event]) this.#listeners[event] = []
this.#listeners[event].push(fn)
}
emit(event, data) {
(this.#listeners[event] || []).forEach(fn => fn(data))
}
}
const shop = new EventEmitter()
shop.on('sale', price => console.log("Email: sale for $" + price))
shop.on('sale', price => console.log("SMS: sale for $" + price))
shop.emit('sale', 9.99)
// Email: sale for $9.99
// SMS: sale for $9.99
You see this in browser events (addEventListener), React state, and most UI frameworks.// without a factory — caller knows too much:
const dog = new Dog("Rex", "Labrador", new DogBrain(), new DogBody())
// with a factory — caller just asks for what they want:
function createPet(type, name) {
if (type === "dog") return new Dog(name, "Labrador", new DogBrain(), new DogBody())
if (type === "cat") return new Cat(name, "Shorthair", new CatBrain())
throw new Error("Unknown pet type: " + type)
}
const myPet = createPet("dog", "Rex") // simple!Some tasks take time — fetching data from the internet, reading a file, waiting for a user to click. Asynchronous code lets your program start that task and continue doing other work while it waits, instead of freezing until the task is done.
// blocking (pseudo-code):
const data = fetch("https://api.example.com/scores") // wait...
// ...and wait...
// ...nothing else happens
console.log(data) // finally continues
// non-blocking:
fetchAsync("https://api.example.com/scores").then(data => {
console.log(data) // runs when data arrives
})
console.log("this prints immediately, before data arrives")// Node.js style — the callback is the last argument:
fs.readFile("data.txt", "utf8", function(err, content) {
// this runs when the file is done reading:
if (err) { console.log("error:", err); return }
console.log(content)
})
console.log("reading started...") // this runs first!
Callbacks work, but nesting many of them creates deeply indented, hard-to-read code — sometimes called "callback hell." Promises were invented to solve this..then() handles success; .catch() handles failure.
fetch("https://api.example.com/players")
.then(response => response.json()) // convert to JSON
.then(data => {
console.log("players:", data) // use the data
})
.catch(err => {
console.log("something went wrong:", err)
})
.finally(() => {
console.log("request complete") // always runs
})
The chain reads top-to-bottom like a story: fetch → parse → use. Much cleaner than nested callbacks.async/await makes asynchronous code look and read like normal synchronous code. Under the hood it's still Promises — it's just nicer to write.
// same as the Promise chain above, but reads like normal code:
async function loadPlayers() {
try {
const response = await fetch("https://api.example.com/players")
const data = await response.json()
console.log("players:", data)
} catch (err) {
console.log("something went wrong:", err)
}
}
// run two things at the same time with Promise.all:
async function loadAll() {
const [players, scores] = await Promise.all([
fetch("/players").then(r => r.json()),
fetch("/scores").then(r => r.json()),
])
console.log(players, scores)
}
await pauses the current function until the Promise resolves — but the rest of the program keeps running.Git tracks every change you make to your code over time. Think of it as a save system that keeps every version forever — you can always go back. It also lets multiple people work on the same project without overwriting each other's work.
# stage a file (mark it as "include this in the next snapshot"):
git add player.js
# stage everything that changed:
git add .
# commit: create a snapshot with a message
git commit -m "add health regeneration to Player class"
# see the history:
git log --oneline
# a3f91c2 add health regeneration to Player class
# 5d2b017 fix score calculation bug
# 9ac1e03 initial commit
Good commit messages are short, specific, and finish the sentence "This commit will...". "Fix bug" is bad. "Fix score doubling on level-up" is good.# create a branch and switch to it:
git checkout -b add-multiplayer
# (make your changes, commit them on this branch)
git add .
git commit -m "add multiplayer lobby"
# switch back to main:
git checkout main
# merge your branch into main:
git merge add-multiplayer
# delete the branch now it's merged:
git branch -d add-multiplayer
This means "main" (or "master") is always the working version, and experimental work lives safely on its own branch.push your commits to share them; you pull to get changes someone else made.
# upload your commits to the remote:
git push origin main
# download new commits from the remote:
git pull
# if you started a project from scratch, link it to a remote:
git remote add origin https://github.com/you/project.git
git push -u origin main
GitHub, GitLab, and Bitbucket are popular hosting services. They also provide pull requests — a way to review someone's branch before merging it.A data structure is a way of organising data in memory. Picking the right one for the job makes your program faster and your code simpler. Here are the ones you'll meet most often.
An array (or list) stores items in a row, each with a numbered position called an index. Reading an item by its index is instant. Searching for a value you don't know the position of means checking each item one by one.
nums = [10, 20, 30, 40]
nums[0] # 10 — instant lookup by index
nums[2] # 30
len(nums) # 4
# searching for a value means scanning:
30 in nums # True, but had to check each itemArrays are the workhorse of programming — most other structures are built on top of them.
A stack is like a pile of plates — you add and remove from the top. Last in, first out (LIFO). The undo button in any app is a stack.
A queue is like a line at a shop — you join the back and leave from the front. First in, first out (FIFO). Print jobs and task lists use queues.
# stack (LIFO):
stack = []
stack.append("a") # push
stack.append("b")
stack.pop() # "b" — the last one added comes out first
# queue (FIFO):
from collections import deque
queue = deque()
queue.append("a") # join the back
queue.append("b")
queue.popleft() # "a" — the first one added comes out firstA hash map stores key → value pairs and can find any value by its key almost instantly — no scanning needed. This is the same structure as Python dicts, JavaScript objects, and Java HashMaps.
ages = { "Aria": 7, "Bryn": 12 }
ages["Aria"] # 7 — instant lookup by key
ages["Cy"] = 3 # instant insert
"Bryn" in ages # True — instant membership checkBehind the scenes it uses hashing — turning each key into a number that points straight to where the value lives. That's why it's so fast.
A tree organises data in a branching hierarchy — one root, with children below it. Your computer's folders are a tree. So is the structure of a web page (the DOM).
A graph is a network of items (nodes) connected by links (edges). Social networks, maps, and the web itself are graphs — friends connect to friends, cities connect by roads.
# a tree as nested data:
company = {
"name": "CEO",
"reports": [
{ "name": "CTO", "reports": [] },
{ "name": "CFO", "reports": [] }
]
}
# a graph as connections:
friends = {
"Aria": ["Bryn", "Cy"],
"Bryn": ["Aria"],
"Cy": ["Aria"]
}An algorithm is a step-by-step recipe for solving a problem. Big O is the way we describe how slow an algorithm gets as the data grows — the single most useful idea for writing fast code.
An algorithm is just a clear sequence of steps. A recipe is an algorithm. Finding the largest number in a list is an algorithm: start with the first, compare each next one, keep the bigger.
def find_max(numbers):
biggest = numbers[0] # step 1: assume the first is biggest
for n in numbers: # step 2: look at each number
if n > biggest: # step 3: if it's bigger...
biggest = n # step 4: ...remember it
return biggest # step 5: hand back the winner
find_max([4, 9, 2, 7]) # 9Big O describes how the amount of work grows as the input grows. It ignores the small stuff and focuses on the shape of the growth. Lower is better.
O(1) — constant. Same work no matter the size (a hash-map lookup).
O(n) — linear. Double the data, double the work (scanning a list).
O(n²) — quadratic. Double the data, four times the work (nested loops).
O(log n) — logarithmic. Adding more data barely changes the work (binary search). Excellent.
# O(1) — one step regardless of size:
first = items[0]
# O(n) — one pass over everything:
for item in items:
print(item)
# O(n squared) — a loop inside a loop:
for a in items:
for b in items:
compare(a, b)Linear search checks every item until it finds the target — O(n). Binary search only works on sorted data: it checks the middle, throws away half, and repeats — O(log n). Finding a name in a sorted phone book by opening to the middle is binary search.
# binary search on a SORTED list:
def binary_search(sorted_list, target):
low, high = 0, len(sorted_list) - 1
while low <= high:
mid = (low + high) // 2 # check the middle
if sorted_list[mid] == target:
return mid # found it
elif sorted_list[mid] < target:
low = mid + 1 # target is in the right half
else:
high = mid - 1 # target is in the left half
return -1 # not foundSorting puts items in order. There are many sorting algorithms with different speeds. The good news: you almost never write one yourself — every language has a fast, well-tested sort built in. Just know that sorting costs about O(n log n), which is why you sort once and search many times.
# use the built-in sort — it's fast and correct:
nums = [5, 2, 8, 1, 9]
nums.sort() # [1, 2, 5, 8, 9]
# sort by a custom rule:
words = ["banana", "fig", "apple"]
words.sort(key=len) # ["fig", "apple", "banana"] — by lengthRecursion is when a function calls itself. It sounds strange at first, but it's the natural way to solve problems that break down into smaller copies of themselves — like exploring folders inside folders.
Every recursive function needs two parts: a base case that stops the recursion, and a recursive case that calls itself on a smaller problem. Without a base case, it would call itself forever and crash.
def countdown(n):
if n == 0: # BASE CASE: stop here
print("liftoff!")
return
print(n)
countdown(n - 1) # RECURSIVE CASE: smaller problem
countdown(3)
# 3
# 2
# 1
# liftoff!The factorial of 4 (written 4!) is 4 × 3 × 2 × 1. Notice that 4! is just 4 × 3!, and 3! is 3 × 2!, and so on. That self-similarity is exactly what recursion captures.
def factorial(n):
if n <= 1: # base case: 1! and 0! are 1
return 1
return n * factorial(n - 1) # n! = n times (n-1)!
factorial(4) # 24
# factorial(4) = 4 * factorial(3)
# = 4 * 3 * factorial(2)
# = 4 * 3 * 2 * factorial(1)
# = 4 * 3 * 2 * 1 = 24Recursion shines for nested structures. To count every file in a folder, you count the files here, then recurse into each sub-folder and add their counts. Trees, menus, and comment threads all work this way.
def count_files(folder):
total = len(folder["files"]) # files right here
for sub in folder["subfolders"]: # then each sub-folder
total += count_files(sub) # recurse into it
return totalAnything you can do with recursion, you can also do with a loop, and vice versa. Loops are often faster and use less memory. Recursion is often clearer for nested data. One danger: each recursive call uses a little memory (a stack frame), so going too deep causes a stack overflow crash.
# the same countdown as a loop — no recursion:
def countdown(n):
while n > 0:
print(n)
n -= 1
print("liftoff!")Testing means writing code that checks your code works — automatically. Good tests catch bugs before your users do, and let you change things later without fear of breaking something.
Manually clicking through your program to check it still works is slow and easy to forget. Automated tests run in seconds and check everything every time. They're a safety net: when you change one part, the tests tell you instantly if you broke another part.
Tests also document what your code is supposed to do — a new developer can read the tests to understand the rules.
A unit test checks one small piece of code in isolation. At its heart is an assertion: a statement that something must be true. If it isn't, the test fails and tells you exactly where.
# the code being tested:
def add(a, b):
return a + b
# the tests:
def test_add():
assert add(2, 3) == 5 # normal case
assert add(-1, 1) == 0 # negatives
assert add(0, 0) == 0 # zeros
# run it — if any assert is false, the test fails loudlyTest-Driven Development (TDD) flips the order: you write the test first, watch it fail, then write just enough code to make it pass. The cycle is called red → green → refactor.
Red: write a failing test for what you want.
Green: write the simplest code to pass it.
Refactor: clean up the code, with the test guarding you.
# 1. RED — write the test first (it fails, no code yet):
def test_is_even():
assert is_even(4) == True
assert is_even(7) == False
# 2. GREEN — write the minimum to pass:
def is_even(n):
return n % 2 == 0
# 3. REFACTOR — improve the code, test keeps it honestTest the normal case, but spend most of your energy on the edge cases — the inputs that break things. Empty lists, zero, negative numbers, very large values, missing data, and unexpected types are where bugs hide.
def test_average():
assert average([2, 4, 6]) == 4 # normal
assert average([5]) == 5 # single item
assert average([]) == 0 # EMPTY — the tricky one!
# what should average([]) even do? The test forces you to decide.Most modern programs talk to other programs over the internet — to fetch data, save things, or log you in. Understanding how that conversation works is essential for almost any real app.
The web runs on a simple back-and-forth. The client (your browser or app) sends a request. The server (a computer somewhere) sends back a response. That's the whole dance: request, response, done.
When you load a web page, your browser is the client asking a server "please send me this page," and the server responds with the HTML.
Requests use HTTP methods that describe intent:
GET — fetch data (read).
POST — send new data (create).
PUT / PATCH — update existing data.
DELETE — remove data.
Responses come with a status code telling you what happened:
200 OK — success.
404 Not Found — that thing doesn't exist.
401 / 403 — you're not allowed.
500 — the server broke.
GET /players -> 200 (here's the list)
POST /players -> 201 (created a new player)
GET /players/999 -> 404 (no such player)
DELETE /players/3 -> 200 (deleted)An API (Application Programming Interface) is the set of URLs a server offers for programs to talk to it. A REST API organises those URLs around resources (like /players or /orders) and uses the HTTP methods above.
The data sent back and forth is almost always JSON — a simple, readable text format of names and values that every language can parse.
// a JSON response from GET /players/1
{
"id": 1,
"name": "Aria",
"level": 7,
"items": ["sword", "shield"]
}Here's the whole loop in practice: ask a server for data, wait for the response, and use it. Because the internet takes time, this is asynchronous (see the Async concept) — you wait for it without freezing.
// fetch some data from an API (JavaScript):
async function loadPlayer(id) {
const response = await fetch("https://api.example.com/players/" + id)
if (!response.ok) {
throw new Error("Request failed: " + response.status)
}
const player = await response.json() // parse the JSON
console.log(player.name) // use it
}A database is where a program stores data permanently, so it survives after the program closes. It's far more powerful than saving to a plain file — built for searching, sorting, and handling lots of data safely.
The most common kind is the relational database, which organises data into tables — like spreadsheets. Each row is one record (one player), and each column is a field (name, level, score).
players table:
+----+-------+-------+-------+
| id | name | level | score |
+----+-------+-------+-------+
| 1 | Aria | 7 | 100 | <- a row (one player)
| 2 | Bryn | 12 | 80 |
+----+-------+-------+-------+
^ a column (one field for everyone)Almost everything you do to stored data is one of four operations, known as CRUD:
Create — add new data.
Read — fetch data.
Update — change existing data.
Delete — remove data.
In SQL (the language relational databases speak), those map to INSERT, SELECT, UPDATE, and DELETE.
-- Create:
INSERT INTO players (name, level) VALUES ('Cy', 3);
-- Read:
SELECT name, score FROM players WHERE level > 5;
-- Update:
UPDATE players SET score = 120 WHERE name = 'Aria';
-- Delete:
DELETE FROM players WHERE name = 'Cy';The "relational" part comes from linking tables together. A primary key uniquely identifies each row (usually an id). A foreign key in one table points to a primary key in another, connecting them.
players table: guilds table:
+----+-------+--------+ +----+----------+
| id | name |guild_id| | id | name |
+----+-------+--------+ +----+----------+
| 1 | Aria | 10 |-->| 10 | Aurora |
| 2 | Bryn | 10 |-->| 10 | Aurora |
+----+-------+--------+ +----+----------+
^ foreign key points to a guild's idThis avoids repeating the guild's full details on every player — you store it once and link to it.
SQL (relational) databases use tables with a fixed structure and are great when data is well-organised and consistent — banking, orders, users. Examples: PostgreSQL, MySQL, SQLite.
NoSQL databases are more flexible — they often store free-form documents (like JSON) without a fixed shape. They're handy for rapidly changing or huge-scale data. Examples: MongoDB, Redis, Firebase.
// a NoSQL document — no fixed columns, just flexible JSON:
{
"name": "Aria",
"level": 7,
"inventory": ["sword", "shield"],
"settings": { "theme": "dark", "sound": true }
}Writing working code isn't enough — it also needs to be safe. You don't need to be a security expert, but a handful of habits will protect you and your users from the most common attacks.
The golden rule of security: treat all input as dangerous until you've checked it. Anything that comes from outside your program — form fields, URLs, uploaded files, API responses — could be malformed or malicious. Always validate it before using it.
def set_age(raw):
# validate BEFORE trusting:
if not raw.isdigit():
raise ValueError("Age must be a number")
age = int(raw)
if age < 0 or age > 150:
raise ValueError("Age out of range")
return ageIf you build database queries by gluing user input directly into text, an attacker can sneak in their own SQL. This is SQL injection, one of the oldest and most damaging attacks. The fix is parameterized queries, which keep data and commands separate.
# DANGEROUS — never glue input into a query:
query = "SELECT * FROM users WHERE name = '" + user_input + "'"
# if user_input is: '; DROP TABLE users; --
# ...you just deleted your whole table.
# SAFE — use parameters; the database treats input as pure data:
cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))Never store passwords as plain text. If your database leaks, every password is exposed. Instead, store a hash — a one-way scramble. You can check a login by hashing what the user typed and comparing, but you can never turn the hash back into the password.
# never do this:
# save_user(name, password) # plain text — disaster waiting to happen
# do this — hash with a proper password hashing library:
import bcrypt
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
save_user(name, hashed) # store the hash, not the password
# checking a login:
bcrypt.checkpw(attempt.encode(), hashed) # True or FalseAPI keys, database passwords, and tokens are secrets. Never write them directly in your code, and never commit them to Git — once pushed, they're effectively public forever. Keep them in environment variables or a secrets manager, separate from the code.
# bad — secret hard-coded in the source:
API_KEY = "sk_live_abc123realkey" # NEVER do this
# good — read it from the environment at runtime:
import os
API_KEY = os.environ["API_KEY"] # set outside the codeEver wondered what actually happens between typing code and the computer doing something? Computers only understand raw numbers, so your readable code has to be translated. How that translation happens shapes how a language behaves.
The code you write is source code — text meant for humans. A processor only understands machine code — raw binary instructions. Something has to translate between the two. That translator is either a compiler or an interpreter.
your code: print("hi")
|
v (translation)
machine code: 10110000 01100001 ...
|
v
the CPU runs it: "hi" appears on screenA compiled language (C, C++, Rust, Go) translates your whole program into machine code before it runs, producing a standalone executable. This is fast to run and catches many errors up front, but you must compile after every change.
An interpreted language (Python, JavaScript, Ruby) translates and runs your code line by line as it goes. This is more flexible and quicker to test, but generally slower at runtime, and some errors only show up when that line runs.
# compiled (roughly):
# write code -> compile -> run the executable
# gcc app.c -o app then ./app
# interpreted (roughly):
# write code -> run it directly, line by line
# python app.pyMany languages sit in between, using a virtual machine or runtime — a program that runs your code. Java compiles to bytecode that runs on the Java Virtual Machine (JVM); JavaScript runs inside the browser's engine or Node. The runtime also handles things like garbage collection (see the Memory concept).
This is why "write once, run anywhere" works: the same bytecode runs on any machine that has the runtime installed.
Real projects rarely run from a single file. A build step bundles your code, pulls in outside libraries (dependencies), and produces something runnable. A package manager downloads and tracks those dependencies for you.
# a package manager installs dependencies your code needs:
npm install # JavaScript — reads package.json
pip install -r requirements.txt # Python
cargo build # Rust — compiles + fetches dependencies
# these read a list of libraries and fetch the right versionsPick a language below — each one gets its own page. Every lesson is kept simple and friendly, starting from your very first line of code and slowly working up. There are seventeen languages to choose from, so start wherever you like.
The lessons keep jargon light, but coding still has a lot of words. Here's a plain-English dictionary you can search any time — kept separate from the languages, so you can browse it on its own.
The goal was never to write code that looks smart. It's to write code that the next person — often you, later — can read, trust, and change without fear.
Run the loop. Hold the principles. Verify before you claim done. That's the whole method.