Types, arithmetic, the truth about floating-point money, f-strings, and decisions — the building blocks of every actuarial calculation.
int, float, str, boolif / elif / else and guard inputs with assertCompanion notebook: lesson-01-python-fundamentals.ipynb — worked examples and self-checking exercises.
cd ~/actuarial_work
uv init foundations --package --python 3.12
cd foundations
uv add numpy scipy matplotlib
uv add --dev jupyterlab pytest ruff
mkdir notebooks tests
code .
Works as-is in PowerShell (Course 0W) and zsh (Course 0M).
--package makes the project an installable package. Code you put in src/foundations/ can be imported from anywhere in the project — notebooks included:
foundations/
├── notebooks/ lesson01.ipynb, lesson01_lab.ipynb…
├── src/foundations/ your reusable code
├── tests/ Lesson 8
└── pyproject.toml
Commit to Git at the end of every session. By Lesson 8 this is a tested library.
What actually runs your code, and three ways to talk to it.
.py file or a notebook cellpython reads it one statement at a timeuv run python — type a line, see the result. Quit with exit().
Read–Eval–Print Loop
uv run python calc.py — runs top to bottom, then exits.
Repeatable jobs
Cells run one at a time; results stay in memory between cells.
Exploration & explanation
The prompt >>> in examples means “typed into the REPL”. In notebooks, the last expression in a cell is displayed automatically (Course 0).
Everything in Python is an object with a type.
| Type | Holds | Examples |
|---|---|---|
int | Whole numbers, any size | 360, -5, 1_000_000 |
float | Real numbers (approximately!) | 0.05, 1.5e6, -0.0 |
str | Text | "TERM", 'P001' |
bool | Truth values | True, False |
>>> type(360)
<class 'int'>
>>> type(0.05)
<class 'float'>
>>> 2 ** 100 # ints never overflow
1267650600228229401496703205376
Underscores make big numbers readable: sum_assured = 250_000. Python ignores them.
annual_rate = 0.05 # a float
term_years = 10 # an int
product = "TERM" # a str
is_smoker = False # a bool
v = 1 / (1 + annual_rate)
pv = 1000 * v ** term_years
term_years = "ten" # legal... but a bad idea
= means “make this name refer to that value” — not mathematical equalitysnake_case, descriptive, no spaces, can’t start with a digitannual_premium beats ap beats x. Your reviewer — and future you — will thank you.
| Expression | Result | Note |
|---|---|---|
7 / 2 | 3.5 | / always gives a float |
7 // 2 | 3 | floor division |
-7 // 2 | -4 | floor, not truncation! |
7 % 2 | 1 | remainder |
2 ** 0.5 | 1.4142135623730951 | power |
True + True | 2 | bool is a kind of int |
Precedence, highest first:
**-* / // %+ --2 ** 2 is -4, not 4. When in doubt, add parentheses: (1 + i) ** -n.
0.1 + 0.2 != 0.3>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
Floats are stored in binary. Just as ⅓ has no exact decimal form, 0.1 has no exact binary form. Every float carries about 15–17 significant digits.
Never test floats with ==. Use math.isclose(a, b) or a tolerance: abs(a - b) < 1e-9.
For valuation and pricing, 15 significant digits is plenty — errors at the 16th digit are far below any materiality threshold. The danger is comparisons and cents, not precision.
Decimal>>> round(2.5), round(3.5) # banker's rounding
(2, 4)
>>> round(2.675, 2) # 2.675 is really 2.67499999...
2.67
round() rounds halves to the even neighbor, and works on the binary value, which may sit just below the half.
from decimal import Decimal, ROUND_HALF_UP
>>> Decimal("0.10") + Decimal("0.20")
Decimal('0.30')
>>> Decimal("2.675").quantize(Decimal("0.01"),
... rounding=ROUND_HALF_UP)
Decimal('2.68')
Build Decimals from strings, not floats.
Rule of thumb: compute with float; round only when presenting. Use Decimal where cents must reconcile exactly — ledgers, billing, statutory filings.
| Expression | Result |
|---|---|
int("42") | 42 |
float("3.5") | 3.5 |
str(1.5) | '1.5' |
int(3.9) | 3 — truncates toward zero |
int(-3.9) | -3 |
int("3.5") | ValueError |
False?>>> bool(0), bool(0.0), bool(""), bool([]), bool(None)
(False, False, False, False, False)
>>> bool("0") # a non-empty string!
True
Data read from files arrives as text. "0" is truthy and "100" + "5" is "1005". Convert first.
f-strings and the format-spec mini-language.
f"{value:>12,.2f}"
< left · > right · ^ center, or _ thousands separator.2f fixed · .1% percent · .3e scientificrate, premium, claims, n = 0.0425, 1234.5, -5321.789, 360
f"{rate:.2%}" # '4.25%'
f"{premium:,.2f}" # '1,234.50'
f"{claims:+,.0f}" # '-5,322' + forces a sign
f"{n:>6}" # ' 360'
f"{premium:>12,.2f}" # ' 1,234.50'
f"{12345678.9:.3e}" # '1.235e+07'
f"{rate=}" # 'rate=0.0425' handy for debugging
i = 0.05
print(f"{'Year':>4} {'v^t':>8} {'PV of 1,000':>12}")
for t in (1, 5, 10, 20):
v_t = (1 + i) ** -t
print(f"{t:>4} {v_t:>8.5f} {1000 * v_t:>12,.2f}")
Year v^t PV of 1,000
1 0.95238 952.38
5 0.78353 783.53
10 0.61391 613.91
20 0.37689 376.89
Loops (for) arrive properly in the next lesson — for now, read it as “do the indented lines for each value of t”.
Match header widths to value widths and every column lines up — no spreadsheet needed.
Comparisons, if / elif / else, and guarding inputs.
age = 47
smoker = False
if age < 18:
rating = "Decline"
elif age <= 50 and not smoker:
rating = "Preferred"
elif age <= 65:
rating = "Standard"
else:
rating = "Refer to underwriter"
print(rating) # Preferred
== != < <= > >=and, or, not18 <= age < 65= assigns; == compares. if age = 47: is a syntax error.
assertloss = -250.0
deductible = 500.0
assert loss >= 0, f"loss must be non-negative, got {loss}"
assert deductible >= 0, "deductible must be non-negative"
AssertionError: loss must be non-negative, got -250.0
assert condition, message stops the program if the condition is falseAssertions can be switched off (python -O). Use them for sanity checks while developing. For validating real inputs, you’ll raise ValueError — Lesson 3.
In notebooks/lesson01_lab.ipynb, build three calculators. Each needs a Markdown cell explaining the rule, code, and a formatted output table.
What does the insurer pay on a loss, given a per-claim deductible and a policy limit?
Social Security tax up to the wage base; Medicare on everything, with a surcharge above a threshold.
Progressive income tax: each slice of income taxed at its own rate.
Write the rule on paper with a worked example before coding. Then check your code reproduces your hand calculation.
Insurer pays the loss above the deductible d, but never more than the limit u:
payment = min(max(loss − d, 0), u)
deductible = 500
limit = 10_000
loss = 2_500
if loss <= deductible:
payment = 0
elif loss - deductible >= limit:
payment = limit
else:
payment = loss - deductible
| Loss | Payment | Branch |
|---|---|---|
| 300 | 0 | below deductible |
| 2,500 | 2,000 | in the layer |
| 12,000 | 10,000 | capped at limit |
if/elif/else version; check all three rowsmin/max version; confirm they agreeasserts: loss ≥ 0, deductible ≥ 0, limit > 0| Component | Rate | Applies to |
|---|---|---|
| Social Security | 6.2% | wages up to the wage base ($184,500 for 2026) |
| Medicare | 1.45% | all wages |
| Additional Medicare | 0.9% | wages above $200,000 |
US rules, simplified to a single filer with one employer. Check the current wage base each year — it’s a parameter, not a constant.
| Wages | Soc. Sec. | Medicare | Total |
|---|---|---|---|
| 60,000 | 3,720.00 | 870.00 | 4,590.00 |
| 184,500 | 11,439.00 | 2,675.25 | 14,114.25 |
| 250,000 | 11,439.00 | 4,075.00 | 15,514.00 |
Store thresholds and rates in clearly named variables at the top of the cell (WAGE_BASE = 184_500). Next year you change one line.
Illustrative brackets — each rate applies only to the income within its band:
| Taxable income | Rate |
|---|---|
| 0 – 10,000 | 10% |
| 10,000 – 40,000 | 15% |
| 40,000 – 100,000 | 25% |
| over 100,000 | 35% |
Worked example, 55,000: 1,000 + 4,500 + 3,750 = 9,250
| Income | Tax | Average rate |
|---|---|---|
| 8,000 | 800.00 | 10.00% |
| 55,000 | 9,250.00 | 16.82% |
| 150,000 | 38,000.00 | 25.33% |
if/elif chain — one branch per bracketint, float, str, bool and check with type()/, //, %, ** results and precedence==Decimalif/elif/else logic and guard it with assertLists, dictionaries, and loops — and using them to sum infinite series, value perpetuities, and amortize loans.