Skip to content
← back to profile

Programming foundations

The concepts every language shares, explained through what they cost rather than what they look like.

01

Values, types and memory

beginner

A variable is a name bound to a value, and the single most useful thing to understand early is what the name actually holds. For a small value such as a number, it usually holds the value itself. For anything larger, an object, a list, a string in most languages, it holds a reference to something stored elsewhere, and copying the variable copies the reference rather than the thing.

That distinction explains an entire category of confusing bugs. Passing a list to a function and finding it changed afterwards is not the function misbehaving, it is both names pointing at one list. Languages that avoid this do so by making values immutable, so there is nothing to change, which is why immutability keeps reappearing as a recommendation from people who have debugged the alternative.

Types are the other half. A static type system checks at compile time that the operations you wrote make sense for the values you have; a dynamic one checks at run time, when the operation happens. Neither is universally better and the trade is well understood: static typing catches a class of mistakes before anything runs and costs you ceremony, dynamic typing gets out of the way and moves those failures into production.

Worth knowing regardless of language: integers have limits and floating point numbers are approximations. 0.1 plus 0.2 does not equal 0.3 in any language using IEEE 754 doubles, because neither value is exactly representable in binary. Money belongs in integers of the smallest unit, or in a decimal type, and never in a float, which is a rule with a long trail of financial incidents behind it.

why this choice

Most early confusion in any language comes from not knowing whether a name holds a value or a reference to one, and most numeric bugs come from treating floating point as though it were mathematics. Both are cheap to learn and expensive to discover.

check yourself

Why does 0.1 + 0.2 === 0.3 evaluate to false in most languages?

ask about this
Answers are generated and can be wrong. The topic above is the reviewed version.
02

Control flow, functions and scope

beginner

Control flow is the small vocabulary every language shares: do this if that, do this repeatedly, stop early, call something else. What differs is the cost of each. An early return is usually clearer than a nested condition, and a loop that mutates a shared accumulator is usually clearer as a map or a fold, but the version your team reads fastest wins over the version a style guide prefers.

A function is the unit of naming, and naming is most of what makes code readable. The useful test is whether the name says what it does without saying how: send_welcome_email is a name, do_email_stuff_v2 is an apology. A function that needs the word and in its name is usually two functions.

Scope decides which names are visible where, and closures are the part that surprises people. A closure is a function that carries the variables it referenced when it was created, which is what makes callbacks and decorators work, and which is also why a loop that creates functions capturing the loop variable can produce a set of functions that all see the same final value.

Arguments are passed either by value or by reference depending on the language and the type, and the practical consequence is the same as with variables: a function given a mutable structure can change what the caller holds. Making that explicit, by returning a new value instead of modifying the argument, removes a category of bug that is very hard to find by reading.

why this choice

Functions are how a program is made comprehensible: each one is a promise that the reader does not need to look inside. A function that changes something the caller could not predict has broken that promise, which is why side effects deserve to be named in the signature or avoided.

check yourself

A loop creates three functions that each capture the loop variable, and all three return the same value when called. Why?

ask about this
Answers are generated and can be wrong. The topic above is the reviewed version.
03

Choosing a data structure

intermediate

Nearly all everyday code is served by four structures, and knowing their costs by heart removes most performance questions before they arise. An array or list gives you position: constant-time access by index, linear-time search. A hash map gives you lookup by key in roughly constant time, at the cost of memory and no ordering. A set is a hash map without values, for membership and deduplication. A queue or stack gives you an order of processing.

The most common avoidable mistake in real code is searching a list inside a loop. Checking whether each of ten thousand items appears in a list of ten thousand is a hundred million comparisons; the same check against a set is ten thousand lookups. It is the same program with one line changed, and it is the difference between two minutes and a few milliseconds.

Ordering is the next question. A sorted structure, a balanced tree or a sorted list with binary search, gives you range queries and nearest-neighbour lookups that a hash map cannot answer at all. If your access pattern includes everything between these two values, that is the tell.

Then there are the specialised few worth recognising when you meet them: a heap for repeatedly taking the smallest item, a trie for prefix search, a bloom filter for a cheap definitely-not-present answer, a ring buffer for a fixed-size stream of recent items. You will rarely implement one; recognising which problem each solves is what stops you writing a slow version by hand.

why this choice

Choosing the structure is choosing the complexity, and it is a decision made once at the start rather than tuned later. Most code that needs optimising does not need a faster language, it needs a hash lookup where it currently has a scan.

check yourself

Checking membership for each of 10,000 items against a list of 10,000 is slow. What changes it most?

ask about this
Answers are generated and can be wrong. The topic above is the reviewed version.
04

Recursion, iteration and state

intermediate

Recursion expresses a problem in terms of a smaller version of itself, and it is the natural shape for anything tree-like: directory trees, nested JSON, parsers, divide and conquer sorts. Iteration expresses the same thing as a loop with explicit state. Every recursion can be rewritten as iteration with a stack, and the choice is about which one reads more clearly for the shape of the data.

The practical constraint is the call stack. Each recursive call consumes a frame, and stacks are finite: a few thousand frames in Python by default, more in most compiled languages but never unlimited. Recursing over a list of a million elements will exhaust it. Some languages optimise tail calls, where the recursive call is the last thing the function does, into a loop; many, including Python and most JavaScript engines in practice, do not.

Memoisation is where recursion becomes practical for overlapping subproblems. The naive recursive Fibonacci recomputes the same values exponentially often, roughly 2 to the power of n calls; caching each result makes it linear. That single change is also the entire idea behind dynamic programming, which sounds like a separate topic and is mostly this observation applied deliberately.

The state question sits underneath both. A loop that mutates variables is easy to write and easy to get subtly wrong when it grows; a recursive or functional version passes state explicitly, which is more verbose and harder to break. Neither is a rule, but when a loop body reaches thirty lines and four mutable variables, the bug you cannot find is usually one of them being updated in the wrong order.

why this choice

Recursion is the right tool for recursive data and the wrong tool for long flat sequences, and the stack is what decides which is which. Memoisation turns the classic exponential recursion into a linear one, which is the whole trick behind most dynamic programming problems.

check yourself

A naive recursive Fibonacci is exponentially slow. What makes it linear?

ask about this
Answers are generated and can be wrong. The topic above is the reviewed version.
05

Errors, exceptions and failure

intermediate

There are two families of error handling and it is worth knowing which one you are in. Exceptions are out-of-band: a failure unwinds the stack until something catches it, so the happy path stays uncluttered and the failure path is invisible in the signature. Returned errors are in-band: a function returns either a value or an error and the caller must deal with both, which makes failure visible and the code longer.

Both fail in the same way, which is silently swallowing the problem. An empty catch block, or an error return assigned and ignored, converts a loud failure into a wrong answer produced quietly. If there is genuinely nothing to do about an error, the minimum is to record it with enough context to identify it later.

The distinction that matters more than the syntax is between expected and unexpected failures. A file not existing, a validation rule failing, a payment being declined: these are outcomes, and modelling them as return values rather than exceptions usually produces clearer code. A null dereference or an out-of-range index is a bug, and it should be loud, uncaught and fixed rather than handled.

Finally, errors should carry context that is useful to whoever reads the log at three in the morning. Failed to save is not an error message; failed to save order 12345 for customer 678, database timeout after 5s is. Wrapping an error as it moves up the stack, adding what each layer knows, is the cheapest debugging investment available.

why this choice

The failure path is code, and it is the code least likely to be tested and most likely to run during an incident. Deciding deliberately which failures are outcomes and which are bugs is what keeps the first kind handled and the second kind visible.

check yourself

Which failure is best modelled as a return value rather than an exception?

ask about this
Answers are generated and can be wrong. The topic above is the reviewed version.
06

Complexity without hand-waving

advanced

Big O describes how the cost of an operation grows with the size of the input, and it deliberately ignores constants. That is its strength and the source of most of its misuse: it tells you which algorithm wins as n grows, and it says nothing about which is faster for the n you actually have. A linear scan of 50 items beats a hash lookup with an expensive hash function, and both are irrelevant next to one network call.

The growth rates worth recognising on sight are constant, logarithmic, linear, linearithmic, quadratic and exponential. The useful boundary is between quadratic and everything below it: at a million items, an n log n algorithm does roughly twenty million operations and a quadratic one does a trillion. That is the difference between a second and a fortnight, and it is why nested loops over large collections are the first thing to look for.

Space complexity gets less attention and causes at least as many incidents. Loading a whole file into memory works until the file is larger than the container's limit, and the failure is an abrupt kill rather than a slow response. Streaming, processing in chunks, and paginating are the answers, and they are much easier to build in from the start than to retrofit.

Then there is amortised cost, which is where dynamic arrays and hash maps live. Appending to a dynamic array is usually constant time and occasionally linear when it grows and copies, and the average across many appends stays constant. That is fine for throughput and not fine for latency: the request that triggers the resize pays for all the ones that did not, which is the same shape as a garbage collection pause and matters for the same reason.

why this choice

Complexity analysis is for choosing between algorithms, not for predicting runtime. The two facts that repay learning are that quadratic algorithms fall off a cliff at scale, and that amortised constant time means one unlucky call pays for everyone else, which shows up in the tail rather than in the average.

check yourself

Appending to a dynamic array is amortised constant time. What does that hide?

ask about this
Answers are generated and can be wrong. The topic above is the reviewed version.