Jako Heiberg
HomeInsightsMusingsPortfolioAboutContact
Jako Heiberg

From DBase III+ to edge computing.
The journey continues.

Pages

  • About
  • Portfolio
  • Resume / CV
  • Insights
  • Musings
  • Archive
  • Contact

© 2026 Jako Heiberg · Cape Town, South Africa · UTC+2

Built with React + Cloudflare Workers. No PHP was harmed.

Back to Insights
Insights

Python From Scratch — Part 3: Booleans & Control Flow

Every `if` you write rests on a value that's either `True` or `False` — but `==` and `is` are not the same question, and the day you confuse them on the number 257 is the day you find out why. Booleans, truthiness, and Python's whitespace-is-the-syntax rule, up close.

August 12, 2026
8 min read
Python

Part 3 of 23 · Level: Beginner · From Hello World to Neural Networks

Every interesting program eventually has to make a decision: is the password right, is the cart empty, did the user actually type a number? Decisions are built out of one tiny, opinionated thing — a value that is either True or False. Get comfortable with that, and the branching almost writes itself.

Booleans: the two-value type

A boolean is Python's smallest type: it holds exactly one of two values, True or False. Mind the capital letters — true is just an undefined name, and Python will complain.

is_raining = True
has_umbrella = False

print(is_raining)          # => True
print(type(is_raining))    # => <class 'bool'>

You rarely type True/False by hand, though. Usually you get them by asking a question about your data — which is what comparisons are for.

Comparison operators

A comparison takes two values and hands you back a boolean:

print(3 < 5)        # => True
print(3 > 5)        # => False
print(10 == 10)     # => True   (equal?)
print(10 != 10)     # => False  (not equal?)
print(7 >= 7)       # => True
print(7 <= 6)       # => False

The one that trips up newcomers from other languages: == is comparison ("are these equal?"), while a single = is assignment ("make this variable hold that value"). Python actually protects you — writing if x = 5: is a syntax error, not a silent bug. Small mercy, gratefully accepted.

Comparisons work on strings too, ordered by Unicode code point (which is roughly alphabetical):

print("apple" < "banana")   # => True
print("Zoo" < "apple")      # => True   (uppercase sorts before lowercase)

And Python lets you chain them, the way maths does:

age = 25
print(18 <= age < 65)       # => True

That reads as "18 is less than or equal to age, and age is less than 65" — no need to spell out the and yourself.

== vs is: the gotcha nobody warns you about

Here is the one that burns people. == asks "are these two values equal?" is asks "are these two the exact same object in memory?" They sound interchangeable right up until they aren't.

a = [1, 2, 3]
b = [1, 2, 3]

print(a == b)   # => True   (same contents)
print(a is b)   # => False  (two different lists that happen to match)

a and b hold the same numbers, so == is happy. But they are two separate lists at two different addresses, so is says no.

Now the part that makes is genuinely treacherous for beginners: Python caches small integers (-5 to 256), so equal values in that range secretly share one object — and equal values outside it don't.

a = 256
b = int("256")          # built at runtime, so not a literal
print(a == b)           # => True
print(a is b)           # => True   (256 is cached — one shared object)

x = 257
y = int("257")
print(x == y)           # => True
print(x is y)           # => False  (257 isn't cached — two objects, same value)

If you ever "tested" identity with is on small numbers and it seemed to work, you were riding the cache — and it will betray you the moment a value crosses 256. Rule of thumb: use == to compare values; reserve is for identity — which in practice means comparing against the singletons None, True, and False.

result = None

if result is None:      # correct, idiomatic
    print("no result yet")

if result == None:      # works, but not the done thing
    print("please don't")

is None is the blessed way to check for None: it is faster, it can't be fooled by a class that redefines ==, and every Python reviewer expects it.

and, or, not

To combine booleans, Python uses actual English words instead of && and ||:

logged_in = True
is_admin = False

print(logged_in and is_admin)   # => False  (needs both)
print(logged_in or is_admin)    # => True   (needs either)
print(not is_admin)             # => True   (flips it)
  • and is True only if both sides are true.
  • or is True if at least one side is true.
  • not flips a boolean.

One detail worth learning early: these short-circuit. and stops the instant it hits something false; or stops the instant it hits something true — because the answer is already decided. That is not just a speed trick, it is a safety one:

name = ""

# name is empty (falsy), so Python never evaluates name[0],
# and this doesn't crash:
if name and name[0] == "A":
    print("starts with A")
else:
    print("no name, no crash")

Output:

no name, no crash

Put your cheap or protective check on the left, and you can safely guard the expensive or risky one on the right.

if / elif / else, and why indentation is the syntax

Now we spend those booleans. An if statement runs a block only when its condition is truthy:

temperature = 30

if temperature > 25:
    print("Warm out.")
    print("Wear shorts.")
elif temperature > 10:
    print("Bring a jacket.")
else:
    print("It's freezing.")

Output:

Warm out.
Wear shorts.

Read it top to bottom: Python checks each condition in order and runs the first block that is true, then skips the rest. elif ("else if") chains alternatives; else catches everything left over. Both are optional — a lone if is perfectly fine.

Here is the thing that makes Python Python: the indentation is the syntax. Most languages use { } to mark a block and treat whitespace as decoration. Python has no braces — the spaces in front of those print lines are what tell the interpreter "these belong to the if." Change the indentation and you change the meaning:

if False:
    print("skipped")
print("always runs")   # not indented, so not part of the if

Output:

always runs

Practical rules: use four spaces per level (the whole community does), stay consistent, and never mix tabs and spaces — Python will reject the file with a TabError. Your editor almost certainly handles this for you; let it.

Truthiness: values that act like booleans

if does not actually demand a True/False. It takes any value and decides whether it is "truthy" or "falsy." The falsy values are a short list worth memorising, because everything else is truthy:

  • False
  • None
  • 0 (and 0.0)
  • "" — the empty string
  • [], {}, () — empty list, dict, tuple
cart = []

if cart:
    print(f"{len(cart)} items")
else:
    print("Your cart is empty.")

Output:

Your cart is empty.

if cart: reads as "if the cart has anything in it." That is cleaner than if len(cart) > 0: and it is the idiomatic way to ask "is this empty?"

But there is a gotcha hiding in plain sight. Truthiness makes 0 and None look identical to an if, and sometimes they mean very different things:

def describe(count):
    if not count:
        return "nothing to report"
    return f"{count} things"

print(describe(0))      # => nothing to report
print(describe(None))   # => nothing to report

If 0 is a legitimate value (a real count of zero) and None means "we don't know yet," collapsing them with if not count: is a bug. When you specifically mean "is this missing," say exactly that: if count is None:.

The conditional expression (Python's ternary)

Sometimes a full if/else is overkill — you just want to pick between two values. Python has a compact one-line form that reads almost like English:

age = 20
label = "adult" if age >= 18 else "minor"
print(label)   # => adult

The shape is value_if_true if condition else value_if_false. Note the order: the result comes first, then the condition. It feels backwards for about a day, then you'll reach for it constantly:

n = 7
parity = "even" if n % 2 == 0 else "odd"
print(parity)   # => odd

Keep it to genuinely simple choices. If you catch yourself nesting these, that is your cue to go back to a proper if/elif/else — readability wins.

A first look at match / case

Python 3.10 added match, a tidier way to branch on the shape of a value than a tall stack of elifs:

def http_message(status):
    match status:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case 500 | 503:            # match several values with |
            return "Server error"
        case _:                    # _ is the catch-all, like else
            return "Something else"

print(http_message(404))   # => Not Found
print(http_message(503))   # => Server error
print(http_message(999))   # => Something else

The _ case is the wildcard — it matches anything you didn't handle, playing the role of else. For a plain value check like this, an if/elif chain would do the same job; match really earns its keep later, when you start destructuring tuples and objects. For now, just know it exists and reads nicely.

Next up

You can now make decisions — but a program that decides just once isn't much of a program. In Part 4: Loops, we put these same booleans to work as conditions that repeat, teaching Python to do the tedious thing ten thousand times without complaint.

Jako Heiberg

Software developer with 40+ years of building things that work. Full-stack, FastAPI, React. Based in Cape Town, working remotely, worldwide.

Read next

From Hello World to Neural Networks

Python From Scratch — Part 4: Loops

You told Python to remove every 2 from the list, and one survived. Loops are the friendliest thing in the language until they quietly skip an item or run until the heat death of the universe — here's every loop you need, and the two traps that get everyone once.

Related Posts

Insights

Python From Scratch — Part 4: Loops

You told Python to remove every 2 from the list, and one survived. Loops are the friendliest thing in the language until they quietly skip an item or run until the heat death of the universe — here's every loop you need, and the two traps that get everyone once.

October 14, 2026
8 min read
Insights

Python From Scratch — Part 2: Variables, Numbers & Strings

`input()` hands you back the string "25", never the number 25 -- and that one fact is behind more first-week Python bugs than anything else. This part covers variables, numbers and strings, plus the type-conversion trick that stops `"3" + 3` from ruining your afternoon.

July 28, 2026
9 min read
Insights

Python From Scratch — Part 1: Hello, World

Getting Python installed and running is the part that quietly stops more beginners than loops, classes, or neural networks ever will. Part 1 takes you from nothing installed — on Windows, macOS, or Linux — to your first running program, with a warning about the apostrophe that bites on line one.

July 23, 2026
8 min read