Programming Foundations

Lesson 1 of 6

Variables & Data Types

"A name is a handle by which we grasp a thing. If the handle is wrong, the hand slips." — Alan Perlis, Epigrams on Programming

In the last Part we learned that memory is a long row of addressable cells, and that every value — a number, a letter, a colour — is ultimately a pattern of bits sitting in some of those cells. A variable is the human end of that machinery: the name you say out loud so you never have to say "the byte at address 140732…" out loud. Get variables right and the rest of programming is arranging them; get them wrong and you will spend your evenings hunting a value that changed when you swear you never touched it.

That last sentence is not a figure of speech. It is the single most common surprise in a beginner's first month, and it comes directly from a misunderstanding of what a variable is.

The question this chapter answers

When you write x = 5, what actually happens in the machine — and why does changing one variable sometimes change another you never mentioned?

Core Concepts

What a variable actually is

Most tutorials tell you a variable is "a box that holds a value." It is a comforting picture, and in some languages it is even true. In Python — and in most modern languages you will meet — it is quietly wrong, and the wrongness is the source of real bugs.

Here is the accurate picture. A value lives somewhere in memory as an object: the integer 5, the string "Okoro", the list [1, 2, 3]. A variable is a name bound to that object — a label tied to it with a piece of string, not a box the object sits inside. When you write x = 5, Python creates (or reuses) an integer object 5 somewhere in memory and points the name x at it. The name knows where the object is; it does not contain the object.

Why does this distinction earn its keep? Because a single object can wear more than one label. Write y = x and you have not copied anything — you have tied a second string to the same 5. For an integer this is harmless. For a list it is the whole ballgame, and we will see it bite in the code below. Hold this sentence in your head for the rest of the chapter: assignment binds a name; it does not copy a value.

The box model isn't a lie everywhere — it describes C, where int x = 5; really does reserve a box and put the bits 00000101 in it. That is why the "box vs label" question is worth confronting head-on rather than hand-waving: which one is true depends on the language, and knowing which tells you exactly when y = x is safe.

The built-in types, and how each becomes bits

A type is the answer to a question the machine cannot skip: this pattern of bits — how should I read it? The same 32 bits are a whole number, a fraction, or four letters depending only on the type the language attaches to them. Python's core built-in types are few, and each maps down to the binary from Part I:

Type Example What it really is underneath
int 42, -7, 10**100 A whole number in binary. Python ints grow to any size — no 32/64-bit ceiling.
float 3.14, -0.5 An IEEE-754 double: sign, exponent, and fraction bits. Approximate by design.
bool True, False One bit of meaning: literally a subclass of int, where True == 1.
str "Okoro" A sequence of Unicode code points — text, stored as numbers per character.

Three of these hold no surprises. The fourth, float, holds a famous one. Because a float has a fixed number of fraction bits, it can only store numbers that happen to be sums of powers of two. The decimal 0.1 is not one of them — in binary it repeats forever, like 1/3 does in decimal — so the machine keeps the closest value it can fit. Do arithmetic on those almost-right values and the error surfaces: 0.1 + 0.2 is not quite 0.3. This is not a Python bug; it is arithmetic on any hardware using IEEE-754, which is nearly all of it. The runnable code below makes it visible so it never surprises you in a bill total.

Strings are numbers too, one layer up. Each character is a Unicode code point — "O" is 79, "é" is 233, an emoji is a larger number — and the string is that sequence encoded as bytes. This is why text has an encoding at all, and why a badly decoded name shows up as mojibake: the bytes were read with the wrong "which number means which letter" table.

Value vs reference, and mutable vs immutable

Now we can cash in the label idea. Bind two names to the same object and mutate the object through one name — the change is visible through both, because there is only one object. But you can only do this to objects that can be changed. And that is the deepest split in the type system:

  • Immutable objects cannot be altered after creation: int, float, bool, str, and tuples. "Changing" one really makes a new object and rebinds the name. x = x + 1 does not edit 5; it computes 6 and points x at it.
  • Mutable objects can be changed in place: list, dict, set. items.append(4) alters the very object other names may be watching.

Put the two ideas together and the rule falls out cleanly. Aliasing (two names, one object) is invisible for immutable types — you can never observe the sharing, because neither name can change the shared object out from under the other. For mutable types the sharing is everything: b = a on a list means b.append(x) changes what a sees. The gotcha in the code below is exactly this, and it is worth running twice.

Why are strings and ints immutable at all — isn't that a limitation? It buys three things. Safety: you can pass a string to a function knowing it cannot be edited behind your back. Sharing: because 5 can never change, Python can keep one 5 object and hand it to everyone, saving memory. Hashability: only immutable objects can be dictionary keys, because a key's identity must not shift after it's filed — which is why you can key a dict on a string but not on a list.

Dynamic vs static typing

Notice what we never wrote above: a type. In C you must declare int x = 5; — the name carries the type, fixed forever, checked before the program ever runs. That is static typing. Python is dynamically typed: the type rides on the object, not the name, and is checked as the program runs. The same name can point at an int now and a str a line later, and Python won't blink until an operation actually doesn't make sense.

This is a genuine trade-off, not a matter of one being modern and the other old:

Dynamic (Python) Static (C, Java, Rust, TypeScript)
When types are checked While running Before running (compile time)
Writing speed Fast — no declarations, easy prototyping Slower — you must say what everything is
Catching errors At runtime, maybe in production At compile time, before users see it
Performance Slower — type checked repeatedly Faster — compiler knows the layout

The honest summary: dynamic typing optimises for the speed of writing and changing code; static typing optimises for catching whole classes of bug before they ship and for raw speed. Neither wins outright, which is why the industry is meeting in the middle — Python's optional type hints (x: int = 5) let you opt into static-style checking with a tool like mypy, keeping the fast feel while buying back some of the safety.

Scope, in one paragraph

One last thread, because it sets up the next chapter. A name doesn't exist everywhere — it lives in a scope. A name bound inside a function is local: it is born when the function runs and vanishes when it returns, invisible to the outside. A name bound at the top of your file is global: readable everywhere. This is deliberate — local scope is what lets two functions both use a variable called i without colliding, the way two houses can both have a "kitchen." Scope is really a story about functions, so we will pick it up properly in the very next chapter.

Seeing it in code

Four things at once: assignment and type(), the float surprise, immutable rebinding (safe), and mutable aliasing (the classic trap). Read each comment, predict the output, then run it:

Python — Try it Yourself
Loading editor…
Output
Click Run to execute the code.

The difference between blocks 3 and 4 is the whole chapter in eight lines. Both do y = x. In block 3 the object is immutable, so the sharing can never be observed. In block 4 the object is mutable, so the sharing is loud and expensive. The names behaved identically; the objects did not.

Everyday analogy

A variable is a nickname, not a person. If your friend Chidi is also called "Chi-Chi" at home, both names reach the same human — phone either one and the same person picks up. That's a list: two names, one object, and a haircut Chidi gets is visible whichever name you called. An integer is more like the number written on a lottery ticket: to "change" it you tear up the ticket and print a new one. The old ticket in someone else's pocket is untouched. Same act of naming; the difference is entirely in whether the thing named can be edited.

History changed here

Early machines had no variables — you referred to memory by raw address, and a program was a wall of numbers. The word "variable" is borrowed straight from algebra, and its arrival in code was revolutionary: FORTRAN, in 1957, let scientists write X = 3.0 and have a compiler figure out where. That single act of naming — divorcing the human's symbol from the machine's address — is what turned programming from wiring into writing. Every variable you name today is a descendant of that idea: the compiler still does the bookkeeping FORTRAN's did, so your head is free for the problem instead of the plumbing.

The engineer's notebook

What experienced engineers actually do with this, day to day:

• They never compare floats with ==. Money is stored in integer kobo/cents or a Decimal; float equality is checked with a tolerance. The 0.1 + 0.2 gotcha has caused real financial bugs.
• They watch for shared mutable defaults and aliases. "Why did editing this list change that one?" is answered on reflex with "you copied the reference, not the list."
• They add type hints at the edges. Function signatures get def total(items: list[int]) -> int: even in dynamic Python, because hints catch mistakes and document intent for the next human — often themselves in six months.

Real-world example — chika.ng Academy

When a student submits a quiz, the Academy loads their attempt from MySQL into a Python/JS object and reads a score: int, a passed: bool, and a name: str — the same primitive types this chapter names, now carrying a certificate. Two details bite exactly here. First, a percentage stored as a float is compared to the pass mark with a tolerance, never raw ==, so 69.99999 doesn't silently fail a student who scored 70. Second, when the code duplicates a list of a student's answers to grade a copy without disturbing the original submission, it takes an explicit copy — because graded = submitted would alias the same list, and marking the copy would quietly rewrite the record we're meant to preserve.

Common Mistakes

  • Believing b = a copies. For lists, dicts, and sets it copies the reference — both names point at one object, and mutating either shows up in both. Use .copy(), list(x), or dict(x) for a real copy.
  • Comparing floats with ==. 0.1 + 0.2 == 0.3 is False. Round first, use a tolerance, or reach for decimal.Decimal / integer units for money.
  • Thinking int has a maximum. In C it does; Python integers grow without limit. 10**100 is fine — no overflow.
  • Confusing = with ==. = binds a name; == asks whether two objects are equal. A stray = in a condition is a classic typo.
  • Assuming a name has a fixed type. In Python the type lives on the object, not the name. x = 5 then x = "hi" is legal — powerful, and occasionally a trap when you didn't mean it.
  • Confusing == with is. == asks "equal value?"; is asks "the very same object?" Use is only for None, never for comparing numbers or strings.
Thought experiment

You write a = 1000 and b = 1000, then ask a is b — same object? Now try a = 5, b = 5, a is b. In CPython the first is often False and the second True — because the interpreter pre-builds and shares a single cached object for every small integer from −5 to 256, but makes a fresh 1000 each time. Nothing about your values differs; only whether the runtime chose to share the object. The lesson isn't the cache — it's this: is asks about object identity, == asks about value, and the day you use is to compare numbers is the day your code works for 5 and mysteriously breaks for 1000.

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Junior
"What's the difference between a mutable and an immutable type? Give an example of each."
Model answer — "An immutable object can't be changed after it's created — like an int, str, or tuple; 'changing' it really makes a new object. A mutable object can be changed in place — like a list or dict; methods such as append alter the same object. It matters because two names can share one mutable object, so a change through one name is visible through the other."
Follow-up they'll throw: "So what does b = a do for a list?" → It binds a second name to the same list. Say why it matters: mutating b mutates what a sees.
Intermediate
"Why is 0.1 + 0.2 != 0.3, and how would you handle money because of it?"
Model answer — "Floats are IEEE-754 with a fixed number of fraction bits, so numbers like 0.1 that aren't sums of powers of two are stored approximately; the tiny errors surface in arithmetic. For money I never use binary floats — I store integer minor units (kobo or cents) or use a Decimal type, and I compare with a tolerance rather than ==. It's not a language bug; it's how the hardware represents fractions."
Follow-up: "Why can a string be a dict key but a list can't?" → Keys must be hashable, which requires immutability, so the key's identity can't shift after it's filed.
Senior
"Walk me through the trade-offs of dynamic vs static typing on a growing codebase, and where you'd draw the line."
Model answer — "Dynamic typing optimises for velocity — no declarations, easy prototyping, quick refactors — at the cost of a class of errors that only surface at runtime and of raw speed, since types are checked repeatedly. Static typing pushes those errors to compile time and lets the compiler generate faster code, but slows the writing. On a small or fast-moving codebase I lean dynamic; as a team and codebase grow, the cost of runtime type bugs outpaces the cost of annotations, so I add gradual typing — Python hints with mypy in CI, or TypeScript over JS — to buy back safety at the edges and public interfaces without giving up the dynamic feel internally. The line moves with team size and blast radius, not fashion."
This is a design answer wearing a typing costume. The senior move is naming the trade-off, then reaching past it to gradual typing and CI enforcement rather than treating it as a binary.
Company lens — how the same topic gets asked
micro1Live and conversational. The AI interviewer wants you to reason aloud about aliasing — talk through what b = a does before you touch the keyboard. A confident, correct mental model reads better than fast code.
GoogleWants precision on the model: object vs name, identity vs equality, why hashability needs immutability. Expect "what exactly is in memory?" and be ready to be exact.
MicrosoftValues clean fundamentals and typing judgment — often via C# / TypeScript. Narrate why static types earn their keep on a large product, not just that they do.
Amazon / AWSOperational lens: the float-in-money bug and the shared-mutable-state bug are the ones that page you at 3am. Tie the concept to a correctness incident and how you'd prevent it.
Hands-on exercise
  1. In the editor above, add c = list1 after the aliasing block, append to c, and print all three names. Predict which lists change before you run it, then explain the result in one sentence.
  2. Write a function that takes a list and returns a sorted copy without disturbing the caller's list. Prove it works by printing the original before and after.
  3. Out loud (as in a micro1 screen), explain in one breath why x = x + 1 never changes any other name bound to the old value of x.

Chapter Summary

  • A variable is a name bound to an object in memory — a label, not a box. Assignment binds a name; it does not copy a value.
  • The core types map straight to bits: int is binary, str is Unicode code points, bool is one bit, float is IEEE-754 — approximate, so 0.1 + 0.2 != 0.3.
  • b = a copies the reference. For mutable objects (list, dict, set) that means shared state; for immutable ones (int, str, tuple) the sharing can never be observed.
  • Immutability buys safety, sharing, and hashability — which is why only immutable objects can be dict keys.
  • Dynamic typing trades early error-catching and speed for writing velocity; static typing makes the opposite trade. Gradual typing (hints + mypy) splits the difference.
  • Names live in scopes — local names vanish when a function returns — which is the doorway to the next chapter.
Narrative bridge

We can now name values, know what each one is, and predict when two names secretly share one object. But a program that only names values is a shopping list — it doesn't do anything yet. The moment you want to name not a value but an action — a reusable piece of behaviour you can call by name, with its own private scope where local variables are born and die — you have reached the next great abstraction. That's where we turn: the Function.

Editorial reflection — beginners think the hard part of variables is the syntax, and it takes ten minutes. The hard part is the mental model — object versus name, sharing versus copying — and it separates people who debug by guessing from people who debug by knowing. Every "why did that value change?" you will ever chase resolves to a sentence in this chapter. Learn the model, not the syntax, and the mysteries stop being mysterious.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.