Programming Fundamentals

Every programming language, no matter how different it looks on the surface, is built from the same few ideas. Once you understand them in one language, picking up a second is mostly a matter of syntax. This article covers those ideas.


🟢 Junior

Variables

A variable is a named container for a value. You give it a name so you can refer to the value later without repeating it everywhere.

age = 25
name = "Alice"
is_active = True
let age = 25;
const name = "Alice";  // const means the binding can't change
let isActive = true;
int age = 25;
String name = "Alice";
boolean isActive = true;

The name is your invention. age, userAge, theUsersAge all work, but age is clearest. Good names are the single most effective way to make code readable.

Types describe what kind of value a variable holds. int holds integers. string holds text. bool holds true or false. Static languages (Java, C#, Go, TypeScript) require you to declare the type or let the compiler infer it. Dynamic languages (Python, JavaScript) track types at runtime.

let count = 0;        // inferred as number
let label = "hello";  // inferred as string

Scope controls where a variable is accessible. A variable declared inside a function only exists inside that function. This isn’t a limitation — it’s what prevents variables from accidentally interfering with each other across your whole program.

Data types

The basic types shared by nearly every language:

Integer — whole numbers. 42, -7, 0. How large depends on the type: int32 holds up to about 2 billion, int64 up to about 9 quintillion.

Float / Double — decimal numbers. 3.14, -0.5. Stored in IEEE 754 binary floating point — 0.1 + 0.2 is not exactly 0.3 in any language that uses this format.

Booleantrue or false. Used for conditions.

String — text. Internally a sequence of characters (or bytes, depending on encoding). "hello", 'world', """multi-line""" — the quote style varies by language.

Array / List — an ordered collection of values. [1, 2, 3]. Access by index: arr[0] is the first element.

Object / Dictionary / Map — key-value pairs. { name: "Alice", age: 30 }. Access by key: obj.name or obj["name"].

Functions

A function is a named, reusable block of code. You define it once, call it many times.

def greet(name):
    return "Hello, " + name + "!"

greet("Alice")  # "Hello, Alice!"
greet("Bob")    # "Hello, Bob!"
function add(a, b) {
  return a + b;
}

const multiply = (a, b) => a * b;  // arrow function — same idea, shorter syntax
func divide(a, b float64) (float64, error) {
  if b == 0 {
    return 0, errors.New("cannot divide by zero")
  }
  return a / b, nil
}

The values you pass in are parameters (or arguments). The value the function sends back is the return value. A function doesn’t have to return anything — it might just perform a side effect (print something, write to a file, update a database).

Keep functions short and focused. A function that does one thing is easier to understand, test, and reuse than one that does five things. If you find yourself naming a function processAndValidateAndSave, split it.

Conditionals

Conditionals run code only when a condition is true.

score = 87

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"
if (score >= 90) {
  grade = 'A';
} else if (score >= 80) {
  grade = 'B';
} else {
  grade = 'F';
}

The condition must evaluate to a boolean. Comparison operators: == or === (equal), != or !== (not equal), >, <, >=, <=. Logical operators: && / and (both must be true), || / or (either must be true), ! / not (inverts).

if age >= 18 and has_id:
    allow_entry()

if not is_logged_in:
    redirect_to_login()

A switch (or match in newer languages) is a cleaner way to compare one value against many possibilities:

switch (day) {
  case 'Monday':
  case 'Tuesday':
  case 'Wednesday':
  case 'Thursday':
  case 'Friday':
    console.log('weekday');
    break;
  case 'Saturday':
  case 'Sunday':
    console.log('weekend');
    break;
}
match day:
    case 'Saturday' | 'Sunday':
        print('weekend')
    case _:
        print('weekday')

Loops

A loop repeats code until a condition is false (or until you tell it to stop).

while — runs as long as the condition is true. Check happens before each iteration.

count = 0
while count < 5:
    print(count)
    count += 1
# prints 0, 1, 2, 3, 4

for — most commonly used to iterate over a sequence.

fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)
for (let i = 0; i < fruits.length; i++) {
  console.log(fruits[i]);
}

for (const fruit of fruits) {   // same result, cleaner
  console.log(fruit);
}

break exits the loop immediately. continue skips the rest of the current iteration and goes to the next.

for number in range(10):
    if number == 3:
        continue   # skip 3
    if number == 7:
        break      # stop at 7
    print(number)
# prints 0, 1, 2, 4, 5, 6

Be careful with infinite loops — a while True that never breaks will run forever and freeze or crash your program.


🟡 Medior

Functions as values

In most modern languages, functions are values — you can store them in variables, pass them as arguments, and return them from other functions.

const double  = n => n * 2;
const square  = n => n * n;
const negate  = n => -n;

function applyAll(value, ...fns) {
  return fns.reduce((v, fn) => fn(v), value);
}

applyAll(3, double, square, negate); // ((3*2)^2)*-1 = -36

This is called higher-order programming. map, filter, and reduce are higher-order functions built into every modern standard library:

numbers = [1, 2, 3, 4, 5]

doubled  = list(map(lambda n: n * 2, numbers))       # [2, 4, 6, 8, 10]
evens    = list(filter(lambda n: n % 2 == 0, numbers)) # [2, 4]
total    = reduce(lambda acc, n: acc + n, numbers, 0)  # 15

Passing a function that says “what to do” to a function that says “when to do it” is one of the most powerful compositional patterns in programming.

Recursion

Recursion is when a function calls itself. It’s a technique for breaking a problem into smaller versions of the same problem.

Every recursive function needs two things: a base case (the condition where it stops) and a recursive case (where it calls itself with a smaller input).

def factorial(n):
    if n <= 1:         # base case
        return 1
    return n * factorial(n - 1)  # recursive case

factorial(5)  # 5 * 4 * 3 * 2 * 1 = 120

Tracing the calls: factorial(5)5 * factorial(4)5 * 4 * factorial(3)5 * 4 * 3 * factorial(2)5 * 4 * 3 * 2 * factorial(1)5 * 4 * 3 * 2 * 1 = 120.

A classic example where recursion is natural: walking a directory tree where each folder may contain more folders.

import os

def list_files(path, depth=0):
    for entry in os.scandir(path):
        print("  " * depth + entry.name)
        if entry.is_dir():
            list_files(entry.path, depth + 1)  # recurse into subdirectory

The risk: if there’s no valid base case, or the input never reaches it, you get infinite recursion and a stack overflow. Python’s default recursion limit is 1000 calls deep.

Scope and closures

Scope is the region of code where a variable is accessible. Most languages use lexical scope — a function can access variables from the scope where it was defined, not where it was called.

function outer() {
  const message = "hello";

  function inner() {
    console.log(message);  // can access message from outer's scope
  }

  return inner;
}

const fn = outer();
fn();  // "hello" — even though outer() has already returned

inner is a closure — a function that remembers the variables from its enclosing scope even after that scope no longer exists. This is how you create private state: the returned function keeps message alive.

def make_multiplier(factor):
    def multiply(n):
        return n * factor  # factor is captured from the outer scope
    return multiply

triple = make_multiplier(3)
triple(5)  # 15
triple(10) # 30

Error handling

Things go wrong: files don’t exist, network requests fail, user input is invalid. Errors need to be handled — ignoring them produces programs that crash mysteriously.

Exceptions (Python, JavaScript, Java, C#): wrap risky code in a try block. If it throws, the catch block runs.

def read_number(path):
    try:
        with open(path) as f:
            return int(f.read().strip())
    except FileNotFoundError:
        print(f"File not found: {path}")
        return None
    except ValueError:
        print("File contents are not a valid number")
        return None
    finally:
        print("Done attempting to read")  # always runs

Return values (Go, Rust): errors are returned as values alongside the result. The caller must check them.

data, err := os.ReadFile("config.json")
if err != nil {
    return fmt.Errorf("reading config: %w", err)
}

The difference isn’t philosophical — it’s about where error handling happens. Exceptions let errors propagate up until something catches them; return-value errors must be handled at every step. Both have trade-offs. The worst outcome is ignoring errors entirely, which both styles make possible.


🔴 Senior

Mutation and immutability

Mutable state — data that changes — is the root of most bugs in complex programs. The more places something can change, the harder it is to reason about what it is at any given moment.

Immutability means values don’t change after creation. Instead of modifying, you create a new version:

// Mutable (risky in concurrent or complex code)
const config = { host: 'localhost', port: 3000 };
config.port = 4000;  // mutates the original

// Immutable pattern — create a new object
const updatedConfig = { ...config, port: 4000 };
# Tuples are immutable in Python — useful for values that shouldn't change
point = (1, 2)
# point[0] = 3  → TypeError: 'tuple' object does not support item assignment

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: float
    y: float

p = Point(1, 2)
# p.x = 3  → FrozenInstanceError

Functional languages (Haskell, Clojure, Elm) treat immutability as the default. Multi-threaded programming becomes much safer when shared state can’t change — concurrent reads of immutable data need no synchronization.

Composition over complexity

As programs grow, complexity grows with them. The main tool for managing it is composition — building large behaviors from small, simple pieces.

A function that does one thing and does it well is composable. A function that does five things is not — you can’t reuse the part you need without dragging along the four things you don’t.

def validate_email(s):       return '@' in s and '.' in s.split('@')[1]
def normalize(s):            return s.strip().lower()
def is_disposable(email):    return email.split('@')[1] in DISPOSABLE_DOMAINS

def process_email(raw):
    email = normalize(raw)
    if not validate_email(email):
        raise ValueError(f"Invalid email: {raw}")
    if is_disposable(email):
        raise ValueError("Disposable email addresses not allowed")
    return email

Each function is testable in isolation. process_email composes them. Changing the validation rule means changing validate_email — nothing else needs to know.

This scales. Systems built from composable pieces stay understandable as they grow. Systems built from large, interdependent blobs of logic don’t.

How the computer actually runs your code

Understanding the execution model helps you reason about performance.

Source code is transformed (compiled or interpreted) into machine instructions the CPU can execute.

The call stack is a region of memory that tracks function calls. Each call pushes a frame (local variables, return address). Each return pops it. Stack overflow happens when the stack runs out of space — typically from unbounded recursion.

The heap is where dynamically allocated memory lives. In languages with garbage collection (Python, JavaScript, Java, Go), the runtime automatically frees heap memory that’s no longer referenced. In C and C++, you manage it manually.

Interpretation vs compilation: Python and Ruby parse and run source code at runtime. C and Go compile source to machine code before running. Java and C# compile to bytecode that a virtual machine runs. The practical effect: compiled languages are faster; interpreted languages have shorter feedback loops.

Understanding this means you know why a deep recursive call crashes (stack), why creating millions of small objects puts pressure on the garbage collector (heap), and why O(n²) algorithms become slow on large inputs (CPU time).