Intuition Why a "scenario matrix" first
When you learn a rule ("Python does X"), the real test is the edge : what happens at zero? at an empty list? when a copy isn't really a copy? This page collects one worked example per corner case , so that after reading it, no Python expression from the parent note can surprise you. We build every symbol from scratch — a // before we use //, a "reference" before we call anything a reference.
Before the table, three words we will lean on:
Definition Three words we will use everywhere
value :: the actual data, e.g. the number 7 or the text "hi".
reference :: a name tag pointing at a value. x = 7 ties the tag x to the value 7. Think of a luggage tag on a suitcase — the tag is not the suitcase.
truthy / falsy :: every value, when placed where a yes/no is expected, secretly answers "am I empty / zero / nothing?" If yes it counts as False (falsy); otherwise True (truthy).
Each row is a class of situation this topic can throw at you. The last column names the worked example that nails it.
#
Case class
The tricky edge
Covered by
A
Division: sign & type
/ vs // vs %, positive operands
Example 1
B
Division: negative operand
floor rounds down , not toward zero
Example 2
C
Division by zero (degenerate)
raises an exception, not inf
Example 3
D
Type mixing / coercion
int + float, str + int fails
Example 4
E
Truthiness: empty / zero / None
[], 0, None, "" all falsy
Example 5
F
Reference vs copy (mutation trap)
aliasing changes the "original"
Example 6
G
range boundaries (batching)
start, stop-exclusive, step
Example 7
H
Scope (LEGB) shadowing
local name hides global
Example 8
I
Word problem (real ML)
steps-per-epoch, last partial batch
Example 9
J
Exam twist (chained everything)
comprehension + % + truthiness
Example 10
We now walk every cell.
Before any example, earn the three symbols on one picture.
Definition The three ways to divide
Take two whole numbers, a dividend n (what we cut) and a divisor d (into pieces of this size).
n / d — true division . Always gives a float. Answers "exactly how many d 's fit, fractions allowed?"
n // d — floor division . Rounds the true answer down to the nearest whole number below it. Answers "how many whole d 's fit?"
n % d — modulo / remainder . What's left over after taking out all whole d 's.
The identity that ties them together:
n = ( n // d ) × d + ( n % d )
Read it as: whole pieces times piece-size, plus the leftover, rebuilds the original.
Why do we even need //? Because in ML you count discrete things — number of training steps, batch indices. You cannot run 31.25 steps. // answers a counting question; / answers a measuring question.
Worked example Example 1 — Case A: positive division trio
dataset_size = 1000, batch_size = 32. Compute steps with /, //, %.
Forecast: guess each of 1000/32, 1000//32, 1000%32 before reading.
1000 / 32 → 31.25.
Why this step? / keeps the fraction, gives a float.
1000 // 32 → 31.
Why this step? Floor of 31.25 is 31 — how many whole batches of 32 fit.
1000 % 32 → 1000 - 31*32 = 1000 - 992 = 8.
Why this step? 8 leftover samples don't fill a 32-batch — the partial last batch .
Verify: identity 31 × 32 + 8 = 992 + 8 = 1000 . ✓ Rebuilds the dividend.
Worked example Example 2 — Case B: floor division with a
negative operand
Compute -7 // 2 and -7 % 2. This is where most people are wrong.
Forecast: you might say − 3 . Python says − 4 . Why?
True division: -7 / 2 = -3.5.
Why this step? Establish the exact value first.
Floor means "largest whole number not greater than − 3.5 ". On the number line, that is − 4 (since − 3 > − 3.5 , − 3 is too big).
Why this step? Floor always rounds toward − ∞ , never toward zero. See the arrow in the figure.
Remainder from the identity: − 7 = ( − 4 ) ( 2 ) + r ⇒ r = − 7 + 8 = 1 .
Why this step? In Python the remainder takes the sign of the divisor (2 > 0 ), so -7 % 2 = 1.
Verify: (-7 // 2) * 2 + (-7 % 2) = ( − 4 ) ( 2 ) + 1 = − 8 + 1 = − 7 . ✓
Common mistake "Floor division just chops off the decimal"
Why it feels right: for positives, 31.25 → 31 looks like chopping.
The fix: it rounds toward − ∞ . For positives chop = floor by luck; for negatives they differ (-3.5 → -4, not -3). Truncation (chop) is what int(-3.5) does → -3. So int() and // disagree on negatives.
Worked example Example 3 — Case C: division by
zero (degenerate input)
batch_size = 0. What does 1000 // 0 do?
Forecast: infinity? None? A crash?
Python evaluates 1000 // 0.
Why this step? There is no whole number of zero-sized batches — the question is meaningless.
It raises ZeroDivisionError: integer division or modulo by zero.
Why this step? Python refuses to invent inf for integers; it signals the error loudly so a bug can't slip past silently (see Exception handling in data pipelines ).
Guard against it:
steps = dataset_size // batch_size if batch_size else 0
Why this step? if batch_size uses truthiness — 0 is falsy, so we skip the dangerous division.
Verify: with batch_size = 0, the guarded expression returns 0 (the else), no crash. ✓ (Note: 1000 / 0.0 on floats gives ZeroDivisionError too in Python — not inf like NumPy would; see NumPy arrays and vectorization .)
Worked example Example 4 — Case D: type mixing & coercion
Evaluate each, and say which fails.
5 + 2.0, int(3.7), float("2.5"), "Epoch " + 5.
Forecast: three succeed, one throws. Which?
5 + 2.0 → 7.0.
Why this step? Mixing int and float promotes to the more general float. No precision lost by widening.
int(3.7) → 3.
Why this step? int() truncates toward zero (drops the fraction), it does not round — so it's 3 , not 4 .
float("2.5") → 2.5.
Why this step? Explicit parse of text into a number.
"Epoch " + 5 → TypeError .
Why this step? str + int has no sensible meaning; Python has strong typing and refuses to guess. Fix: "Epoch " + str(5) → "Epoch 5".
Verify: 5 + 2.0 == 7.0 ✓, int(3.7) == 3 ✓, float("2.5") == 2.5 ✓, and "Epoch " + str(5) == "Epoch 5" ✓.
Worked example Example 5 — Case E: truthiness of every "empty/zero/nothing"
For each value, what does bool(value) give?
[], [0], 0, 0.0, "", "0", None, {}, {"a":1}.
Forecast: careful — [0] and "0" are traps.
Empty containers are falsy : bool([]) = False, bool("") = False, bool({}) = False.
Why this step? "Am I empty?" → yes → False.
Zero numbers are falsy : bool(0) = False, bool(0.0) = False.
None is falsy : bool(None) = False.
Non-empty containers are truthy even if their contents are zero: bool([0]) = True (the list has one item — it's not empty!), bool("0") = True (a 1-character string), bool({"a":1}) = True.
Why this step? Truthiness of a container asks about its length , not its elements.
Verify: the four falsy: [], 0, 0.0, "", None, {} → all False; the truthy [0], "0", {"a":1} → all True. ✓
Worked example Example 6 — Case F: reference vs copy (the mutation trap)
weights_original = [ 0.5 , 0.3 , 0.8 ]
weights_backup = weights_original
weights_backup[ 0 ] = 0.9
What is weights_original[0] now?
Forecast: 0.5 (backup untouched) — or 0.9?
weights_backup = weights_original copies the reference (name tag) , not the list.
Why this step? Assignment ties a new tag to the same suitcase — see the two arrows into one box in the figure.
weights_backup[0] = 0.9 reaches into that one shared list and edits slot 0.
Why this step? Both tags see the change because there is only one list.
So weights_original[0] is now 0.9.
To truly protect the original: import copy; weights_backup = copy.deepcopy(weights_original) makes a second suitcase .
Verify: after the aliasing, weights_original == [0.9, 0.3, 0.8]. After a deepcopy then backup[0]=0.9, the original stays [0.5, 0.3, 0.8]. ✓
Worked example Example 7 — Case G:
range boundaries for batching
List every value of range(0, 100, 32) and of range(50) (first & last only).
Forecast: does range(0,100,32) include 96? Does it include 100?
range(start, stop, step) yields start, start+step, … while strictly less than stop.
Why this step? stop is exclusive — the fence post you stop before .
range(0, 100, 32) → 0, 32, 64, 96. Next would be 128 ≥ 100, excluded. 96 < 100 so included; 100 never appears.
Why this step? These are exactly the batch start indices for a 100-row dataset, batch size 32 (last batch = rows 96..99, only 4 rows).
range(50) means range(0, 50, 1) → first is 0, last is 49, count is 50.
Why this step? Loop runs epochs times numbered from 0.
Verify: list(range(0,100,32)) == [0,32,64,96]; sum of that list = 192 ; range(50) has last element 49 and length 50. ✓
Worked example Example 8 — Case H: LEGB scope shadowing
learning_rate = 0.01 # Global
def train ():
learning_rate = 0.001 # Local, shadows global
def show ():
return learning_rate # Which one?
return show()
What does train() return, and what is the global learning_rate after?
Forecast: 0.01 (global) or 0.001 (enclosing)?
Inside show, name learning_rate is searched L→E→G→B .
Why this step? Local of show? none. Enclosing train? yes — 0.001. Search stops there.
So train() returns 0.001.
The global was never reassigned (the train-local one shadowed it), so the module-level learning_rate is still 0.01.
Why this step? A bare learning_rate = … inside a function makes a new local , it does not touch the global.
Verify: train() == 0.001 and the untouched global == 0.01. ✓
Worked example Example 9 — Case I: real-world word problem (steps per epoch)
You have 1000 samples, batch size 32 , training for 50 epochs . (a) How many full-batch updates happen per epoch? (b) How many samples in the last, partial batch? (c) Total weight updates over all epochs if you include the partial batch?
Forecast: jot your three numbers.
Full batches per epoch: 1000 // 32 = 31.
Why this step? Counting whole batches → floor division.
Partial batch size: 1000 % 32 = 8.
Why this step? Leftover samples that still form one (smaller) update.
Updates per epoch including the partial batch: 31 + 1 = 32 (since remainder = 0 ). This equals ceil(1000/32).
Why this step? The 8 leftover samples still trigger one update.
Over 50 epochs: 32 × 50 = 1600 updates.
Verify: 31 ⋅ 32 + 8 = 1000 ✓; updates per epoch = 32 ; total = 32 × 50 = 1600 ✓.
Worked example Example 10 — Case J: exam twist (comprehension + modulo + truthiness)
Predict the exact output list:
data = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 ]
result = [x ** 2 for x in data if x % 2 == 0 and x]
Forecast: watch the and x — it drops something.
x % 2 == 0 keeps even numbers: 0, 2, 4, 6.
Why this step? Remainder on division by 2 is 0 exactly for evens.
and x additionally requires x to be truthy ; 0 is falsy → dropped.
Why this step? even AND non-zero. So survivors: 2, 4, 6.
Square them: 4, 16, 36.
Why this step? The comprehension's output expression is x**2.
Verify: [x**2 for x in [0,1,2,3,4,5,6] if x % 2 == 0 and x] == [4, 16, 36]. ✓
Recall Rapid self-check
1000 // 32 and 1000 % 32 ::: 31 and 8
-7 // 2 in Python ::: -4 (rounds toward − ∞ )
Value of 1000 // 0 ::: raises ZeroDivisionError
int(3.7) vs round(3.7) ::: 3 (truncate) vs 4 (round)
Is [0] truthy? ::: Yes — it has length 1, not empty
After b = a; b[0]=9, what is a[0]? ::: 9 (same list, shared reference)
list(range(0,100,32)) ::: [0, 32, 64, 96]
Which values are falsy? ::: [], 0, 0.0, "", None, {}
Mnemonic The floor-division compass
// always points the answer DOWN the number line (toward − ∞ ). int() always points toward ZERO . Same for positives, opposite for negatives.
See also: Python functions and lambda , Python iterators and generators , Pandas DataFrames , Object-oriented programming in Python , TensorFlow and PyTorch basics .