Python Fundamentals & Financial Logic
Types, arithmetic, the truth about floating-point money, f-strings, and decisions — the building blocks of every actuarial calculation.
Lesson 1 · OverviewBy the end of this lesson you can…
- Run Python three ways: the REPL, a script, and a notebook
- Use the four core types:
int,float,str,bool - Predict arithmetic results — including floating-point surprises
- Convert safely between types
- Produce clean financial output with f-strings
- Write decisions with
if/elif/elseand guard inputs withassert
Companion notebook: lesson-01-python-fundamentals.ipynb — worked examples and self-checking exercises.
SetupOne project for the whole course
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.
The Python Runtime
What actually runs your code, and three ways to talk to it.
RuntimePython is an interpreter
.py file or a notebook cellpython reads it one statement at a timeREPL
uv run python — type a line, see the result. Quit with exit().
Read–Eval–Print Loop
Script
uv run python calc.py — runs top to bottom, then exits.
Repeatable jobs
Notebook
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).
Values, Types & Arithmetic
Everything in Python is an object with a type.
TypesThe four core types
| 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.
VariablesNames point at values
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 equality- Python is dynamically typed: a name can point to any type, and can be re-pointed
- Names:
snake_case, descriptive, no spaces, can’t start with a digit
annual_premium beats ap beats x. Your reviewer — and future you — will thank you.
ArithmeticOperators and precedence
| 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:
**- unary
- *///%+-
-2 ** 2 is -4, not 4. When in doubt, add parentheses: (1 + i) ** -n.
Floating pointWhy 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.
Floating pointRounding, and exact money with 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.
ConversionsMoving between types
| 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 |
Truthiness: what counts as 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.
Formatting Financial Output
f-strings and the format-spec mini-language.
f-stringsThe format spec, piece by piece
f"{value:>12,.2f}"
Align
< left · > right · ^ centerWidth
minimum characters — lines up columnsGrouping
, or _ thousands separatorPrecision & type
.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
f-stringsA tidy results table
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.
Decisions
Comparisons, if / elif / else, and guarding inputs.
ConditionalsBranching on conditions
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
- Conditions are checked top to bottom; the first true branch runs, the rest are skipped
- The colon and indentation (4 spaces) define the block — there are no braces
- Comparisons:
==!=<<=>>= - Combine with
and,or,not - Chain ranges naturally:
18 <= age < 65
= assigns; == compares. if age = 47: is a syntax error.
ConditionalsGuarding inputs with assert
loss = -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, messagestops the program if the condition is false- A crash with a clear message beats a silently wrong reserve
Assertions can be switched off (python -O). Use them for sanity checks while developing. For validating real inputs, you’ll raise ValueError — Lesson 3.
Actuarial Lab · 75 minFinancial logic with conditionals
In notebooks/lesson01_lab.ipynb, build three calculators. Each needs a Markdown cell explaining the rule, code, and a formatted output table.
1 · Deductible & limit
What does the insurer pay on a loss, given a per-claim deductible and a policy limit?
2 · Payroll tax
Social Security tax up to the wage base; Medicare on everything, with a surcharge above a threshold.
3 · Tax brackets
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.
Lab · Part 1Deductible and limit
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 |
Tasks
- Write the
if/elif/elseversion; check all three rows - Write the one-line
min/maxversion; confirm they agree - Add
asserts: loss ≥ 0, deductible ≥ 0, limit > 0 - Edge cases: loss exactly 500? Exactly 10,500?
Lab · Part 2Social Security & Medicare (employee share)
| 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.
Lab · Part 3Progressive tax brackets
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% |
Tasks
- Solve it with an
if/elifchain — one branch per bracket - Print tax, marginal rate, and average rate with f-strings
- Stretch: notice the repetition? In the next lesson, loops remove it.
RecapLesson 1 checklist
- I can run code in the REPL, a script, and a notebook
- I know
int,float,str,booland check withtype() - I can predict
/,//,%,**results and precedence - I never compare floats with
==
- I know when to reach for
Decimal - I convert text to numbers before calculating
- I can format currency, percentages, and aligned columns
- I can write
if/elif/elselogic and guard it withassert
Collections, Iteration & Mathematical Series
Lists, dictionaries, and loops — and using them to sum infinite series, value perpetuities, and amortize loans.