Drill:
drills/d00_python_basics.py· Test:uv run pytest phases/01-foundations/part-a/tests/test_00_python_basics.py
The language itself: variables, numbers, strings, booleans, if/elif/else, and loops.
Start here if Python is new. If it is a refresher, read the gotcha callouts and skip the
rest — they are the parts that still bite people who have written Python for years.
Everything in topics 01 onward assumes this.
Variables
model = "claude-sonnet-5"
max_tokens = 1024
temperature = 0.7
streaming = True
No declaration keyword, no type required. A variable is a name bound to an object — and you can rebind it to a different type at any time. Python will let you; pyright will not, once the name has a known type, which is the point of the annotations you will write everywhere.
Annotate when the type is not obvious from the value:
retries: int = 0
last_error: str | None = None # the value says nothing about the None case
snake_case for variables, UPPER_SNAKE for constants. AGENTS.md requires it and ruff enforces it.
Numbers
Two types you will actually use: int (whole, unlimited size) and float (decimal).
tokens = 1500 # int
price = 0.003 # float
big = 1_500_000 # underscores are ignored — purely for readability
Arithmetic:
7 + 2 # 9
7 - 2 # 5
7 * 2 # 14
7 / 2 # 3.5 <- true division, ALWAYS returns a float
7 // 2 # 3 <- floor division, stays an int
7 % 2 # 1 <- remainder
7**2 # 49 <- power
Gotcha:
/always produces afloat, even when it divides evenly.10 / 5is2.0, not2. If you need an int — a token count, an index, a batch size — use//. Passing a float where a provider expects an integermax_tokensis a 400 error.
Floats are approximate:
0.1 + 0.2 == 0.3 # False
round(0.1 + 0.2, 10) == 0.3 # True
This is binary floating point, not a Python quirk — every language does it. It matters when
you compare computed costs. Compare with a tolerance, or use round(). In tests, pytest gives
you pytest.approx() for exactly this.
Converting:
int("42") # 42
float("0.7") # 0.7
str(42) # '42'
int(3.9) # 3 — truncates toward zero, does NOT round
round(3.9) # 4
Strings
name = "claude"
also_fine = 'claude' # single and double quotes mean the same thing
multi = """line one
line two"""
Both quote styles work identically. This repo standardises on double quotes because ruff
rewrites single to double on format — so write " and save yourself the churn.
The methods you will actually reach for:
" hi ".strip() # 'hi' — trims whitespace both ends
"Claude".lower() # 'claude'
"claude".upper() # 'CLAUDE'
"a-b-c".replace("-", "_") # 'a_b_c'
"a,b,c".split(",") # ['a', 'b', 'c']
",".join(["a", "b"]) # 'a,b' — note: the separator calls join
"claude-5".startswith("claude") # True
"model" in "model_name" # True — substring test
len("claude") # 6
"claude"[0] # 'c'
Gotcha: strings are immutable.
text.strip()does not changetext, it returns a new string.text.strip()on its own line does nothing at all — you must assign the result:text = text.strip(). Same for.lower(),.replace(), and every other string method.
f-strings embed expressions directly:
f"{model} at {temperature}" # 'claude-sonnet-5 at 0.7'
f"{tokens:,}" # '1,500' — thousands separators
f"{price:.4f}" # '0.0030' — 4 decimal places
f"{ratio:.1%}" # '85.0%' — as a percentage
f"{tokens=}" # 'tokens=1500' — self-documenting debug output
Topic 01 goes further on formatting; this is enough to start.
Booleans and comparison
True, False # note the capitals
5 > 3 # True
5 == 5 # equality — TWO equals signs
5 != 3 # not equal
0 <= temperature <= 2 # chaining works, and reads exactly as it looks
Gotcha:
=assigns,==compares.if x = 5:is a syntax error in Python (unlike C), which is a mercy.
Combining:
a and b # both
a or b # either
not a # negation
and/or short-circuit — they stop as soon as the answer is known:
if value is not None and value.strip(): # .strip() never runs when value is None
That ordering is load-bearing. Reversed, it raises AttributeError on None. You will use
this pattern constantly for optional API fields.
is vs ==:
x == y # same VALUE — what you almost always want
x is y # same OBJECT in memory
Use is only for None, True, and False. value is None is correct; name is "claude"
is a bug that happens to work sometimes.
if / elif / else
if word_count > 2000:
size = "long"
elif word_count > 500:
size = "medium"
else:
size = "short"
Indentation is the syntax. No braces. Four spaces, consistently — ruff enforces this.
Clauses are checked top to bottom and the first match wins; the rest are skipped. That is
why the order above works: by the time elif word_count > 500 runs, you already know the
count is not above 2000. Write these ranges from widest to narrowest, or they collapse into
one branch.
There is also a conditional expression, for when you are picking between two values:
label = "long" if word_count > 2000 else "short"
Fine for two outcomes. For three or more, use the statement form.
for loops
for word in text.split():
print(word)
for i in range(5): # 0, 1, 2, 3, 4
print(i)
for i in range(1, 6): # 1, 2, 3, 4, 5
print(i)
Python’s for iterates over items, not indices. There is no for (i = 0; i < n; i++).
Gotcha:
range(5)stops before 5. Ranges and slices in Python are always “start inclusive, stop exclusive.” It is consistent everywhere, which makesrange(a, b)produce exactlyb - aitems.
The accumulator pattern, which you will write endlessly:
total = 0
for count in token_counts:
total = total + count # or: total += count
+= is shorthand for “add and rebind.” -=, *=, //= work the same way.
while loops
Use for when you know what you are iterating over, while when you are waiting on a
condition:
delay = 1
while delay < 60:
print(f"retrying in {delay}s")
delay = delay * 2 # 1, 2, 4, 8, 16, 32
Gotcha: something inside the loop must eventually make the condition false. Forget the
delay = delay * 2line and it runs forever. Every infinite loop you ever write will be a missing update like this one.
That doubling is real: it is exponential backoff, which you will implement against a rate-limited provider in Phase 2.
break and continue
for line in lines:
if not line.strip():
break # stop the loop entirely
for count in counts:
if count < 0:
continue # skip to the next iteration
total += count
break exits. continue skips the rest of this pass and starts the next. Both apply only to
the innermost loop.
continue is the clean way to say “this item does not qualify” without indenting the real work
inside an if.
Where this shows up later
Everywhere — this is the language. Specifically:
- Topic 01 builds directly on strings, truthiness, and
if/else. - Topic 04 turns
ifchecks into raised exceptions. - Phase 2 — exponential backoff is the
whileloop above with a sleep in it.
Do the drills
drills/d00_python_basics.py. Nine functions, deliberately small. If they feel easy, you are
ready for topic 01 — go.