Skip to content

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.

Check yourself

Where should a monetary amount be stored?

the reference was copiedthe object was notanameb = anameReferencecopied on assignmentOne listmutated througheither name
ClientEdge / CDNData storeDashed = asynchronous
A name holds a reference, so two names can hold the same thing
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.

Check yourself

A function modifies a structure the caller passed in. What is the cost?

captured by referencefresh each timeafter the loop endsLoopi = 0, 1, 2One bindingshared by all threePer-iteration bindAll three see 3Each sees its own
ClientServiceData storeDashed = asynchronousConsidered, not chosen
A closure captures the variable, not a copy of its value
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.

Check yourself

What is a heap the right structure for?

What is asked?By positionBy keyEverything betweenSmallest, oftenArrayO(1) index, O(n)searchHash mapO(1) lookup, no orderSorted treeordered, O(log n)Heapcheap extreme
ClientServiceData storeDashed = asynchronous
The access pattern chooses the structure
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: CPython's default recursion limit is 1,000, and while most compiled languages allow far more, none allow 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: the call count is exactly two times the next Fibonacci number minus one, so it grows as roughly 1.6 to the power of n rather than the 2 to the n that gets quoted. 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.

Check yourself

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

still bounded by framesfib(n)Naive recursionrecomputes everythingMemoisedeach result cachedAbout 2^n callsAbout n callsStack depthfinite either way
ClientServiceExternalData storeDashed = asynchronousConsidered, not chosen
The same subproblem, solved once or solved exponentially often
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.

Check yourself

Which failure should be loud and uncaught rather than handled?

the tempting pathAn operationExpected outcomedeclined, not foundBugnull, out of rangeReturned valuecaller must handle itUncaughtfix it, do not handleitEmpty catchwrong answer, quietly
ClientServiceData storeExternalDashed = asynchronousConsidered, not chosen
Expected outcomes belong in the signature; bugs belong loud
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.

Check yourself

Which comparison should decide between two algorithms in production code?

n = 1,000,000log nabout 20 operationsn log nabout 20 millionn squaredabout a trillionInstantAbout a secondAbout a fortnight
ClientData storeServiceExternalDashed = asynchronousConsidered, not chosen
At a million items, the class is the whole story
Ask about this

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